diff --git a/assets/svg/buy/Simplex-Nuvei-Logo-light.svg b/assets/svg/buy/Simplex-Nuvei-Logo-light.svg new file mode 100644 index 000000000..367ce0de4 --- /dev/null +++ b/assets/svg/buy/Simplex-Nuvei-Logo-light.svg @@ -0,0 +1,65 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/assets/svg/buy/Simplex-Nuvei-Logo.svg b/assets/svg/buy/Simplex-Nuvei-Logo.svg new file mode 100644 index 000000000..736f6fd45 --- /dev/null +++ b/assets/svg/buy/Simplex-Nuvei-Logo.svg @@ -0,0 +1,65 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/assets/svg/coin_icons/BinanceUSD.svg b/assets/svg/coin_icons/BinanceUSD.svg new file mode 100644 index 000000000..d2a374781 --- /dev/null +++ b/assets/svg/coin_icons/BinanceUSD.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/assets/svg/coin_icons/Cosmos.svg b/assets/svg/coin_icons/Cosmos.svg new file mode 100644 index 000000000..a97174439 --- /dev/null +++ b/assets/svg/coin_icons/Cosmos.svg @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000..75de37346 --- /dev/null +++ b/assets/svg/coin_icons/Dai.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/assets/svg/coin_icons/Dash.svg b/assets/svg/coin_icons/Dash.svg new file mode 100644 index 000000000..140ab6704 --- /dev/null +++ b/assets/svg/coin_icons/Dash.svg @@ -0,0 +1 @@ +d \ No newline at end of file diff --git a/assets/svg/coin_icons/EOS.svg b/assets/svg/coin_icons/EOS.svg new file mode 100644 index 000000000..df772834f --- /dev/null +++ b/assets/svg/coin_icons/EOS.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/assets/svg/coin_icons/Ethereum.svg b/assets/svg/coin_icons/Ethereum.svg new file mode 100644 index 000000000..7ffd694cc --- /dev/null +++ b/assets/svg/coin_icons/Ethereum.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/svg/coin_icons/Litecoin.svg b/assets/svg/coin_icons/Litecoin.svg index 2b89ca50b..13e76a40e 100644 --- a/assets/svg/coin_icons/Litecoin.svg +++ b/assets/svg/coin_icons/Litecoin.svg @@ -1,11 +1 @@ - - - - - - - - - - - +litecoin-ltc-logo \ No newline at end of file diff --git a/assets/svg/coin_icons/Ripple.svg b/assets/svg/coin_icons/Ripple.svg new file mode 100644 index 000000000..9a2c7c632 --- /dev/null +++ b/assets/svg/coin_icons/Ripple.svg @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/assets/svg/coin_icons/Stellar.svg b/assets/svg/coin_icons/Stellar.svg new file mode 100644 index 000000000..02afb7b79 --- /dev/null +++ b/assets/svg/coin_icons/Stellar.svg @@ -0,0 +1 @@ +Asset 1 \ No newline at end of file diff --git a/assets/svg/coin_icons/Tether.svg b/assets/svg/coin_icons/Tether.svg new file mode 100644 index 000000000..e53082240 --- /dev/null +++ b/assets/svg/coin_icons/Tether.svg @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000..fa87a1d7e --- /dev/null +++ b/assets/svg/coin_icons/Tron.svg @@ -0,0 +1 @@ +tron \ No newline at end of file diff --git a/lib/hive/db.dart b/lib/hive/db.dart index f5e031972..5c4f252e6 100644 --- a/lib/hive/db.dart +++ b/lib/hive/db.dart @@ -33,6 +33,7 @@ class DB { static const String boxNameDBInfo = "dbInfo"; static const String boxNameTheme = "theme"; static const String boxNameDesktopData = "desktopData"; + static const String boxNameBuys = "buysBox"; String boxNameTxCache({required Coin coin}) => "${coin.name}_txCache"; String boxNameSetCache({required Coin coin}) => diff --git a/lib/main.dart b/lib/main.dart index b2f75705d..8296d739a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,6 +41,7 @@ import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/route_generator.dart'; +// import 'package:stackwallet/services/buy/buy_data_loading_service.dart'; import 'package:stackwallet/services/debug_service.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; @@ -292,10 +293,14 @@ class _MaterialAppWithThemeState extends ConsumerState // unawaited(_nodeService.updateCommunityNodes()); // run without awaiting - if (Constants.enableExchange && - ref.read(prefsChangeNotifierProvider).externalCalls && + if (ref.read(prefsChangeNotifierProvider).externalCalls && await ref.read(prefsChangeNotifierProvider).isExternalCallsSet()) { - unawaited(ExchangeDataLoadingService().loadAll(ref)); + if (Constants.enableExchange) { + unawaited(ExchangeDataLoadingService().loadAll(ref)); + } + // if (Constants.enableBuy) { + // unawaited(BuyDataLoadingService().loadAll(ref)); + // } } if (ref.read(prefsChangeNotifierProvider).isAutoBackupEnabled) { @@ -312,6 +317,11 @@ class _MaterialAppWithThemeState extends ConsumerState break; } } + + // ref + // .read(prefsChangeNotifierProvider) + // .userID; // Just reading the ref should set it if it's not already set + // We shouldn't need to do this, instead only generating an ID when (or if) the userID is looked up when creating a quote } catch (e, s) { Logger.print("$e $s", normalLength: false); } diff --git a/lib/models/buy/buy_form_state.dart b/lib/models/buy/buy_form_state.dart new file mode 100644 index 000000000..843262eb3 --- /dev/null +++ b/lib/models/buy/buy_form_state.dart @@ -0,0 +1,13 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:stackwallet/services/buy/buy.dart'; + +class BuyFormState extends ChangeNotifier { + Buy? _buy; + Buy? get buy => _buy; + set buy(Buy? value) { + _buy = value; + } + + bool reversed = false; +} diff --git a/lib/models/buy/response_objects/crypto.dart b/lib/models/buy/response_objects/crypto.dart new file mode 100644 index 000000000..d3f3aded0 --- /dev/null +++ b/lib/models/buy/response_objects/crypto.dart @@ -0,0 +1,44 @@ +class Crypto { + /// Crypto ticker + final String ticker; + + /// Crypto name + final String name; + + /// Crypto network + final String? network; + + /// Crypto contract address + final String? contractAddress; + + Crypto({ + required this.ticker, + required this.name, + required this.network, + required this.contractAddress, + }); + + factory Crypto.fromJson(Map json) { + try { + return Crypto( + ticker: "${json['ticker']}", + name: "${json['name']}", + network: "${json['network']}", + contractAddress: "${json['contractAddress']}", + ); + } catch (e) { + rethrow; + } + } + + Map toJson() { + final map = { + "ticker": ticker, + "name": name, + "network": network, + "contractAddress": contractAddress, + }; + + return map; + } +} diff --git a/lib/models/buy/response_objects/fiat.dart b/lib/models/buy/response_objects/fiat.dart new file mode 100644 index 000000000..00cb0f945 --- /dev/null +++ b/lib/models/buy/response_objects/fiat.dart @@ -0,0 +1,45 @@ +import 'package:decimal/decimal.dart'; + +class Fiat { + /// Fiat ticker + final String ticker; + + /// Fiat name + final String name; + + /// Fiat name + final Decimal minAmount; + + /// Fiat name + final Decimal maxAmount; + + Fiat( + {required this.ticker, + required this.name, + required this.minAmount, + required this.maxAmount}); + + factory Fiat.fromJson(Map json) { + try { + return Fiat( + ticker: "${json['ticker']}", + name: "${json['name']}", // TODO nameFromTicker + minAmount: Decimal.parse("${json['minAmount'] ?? 0}"), + maxAmount: Decimal.parse("${json['maxAmount'] ?? 0}"), + ); + } catch (e) { + rethrow; + } + } + + Map toJson() { + final map = { + "ticker": ticker, + "name": name, + "min_amount": minAmount, + "max_amount": maxAmount, + }; + + return map; + } +} diff --git a/lib/models/buy/response_objects/order.dart b/lib/models/buy/response_objects/order.dart new file mode 100644 index 000000000..3802dd1cc --- /dev/null +++ b/lib/models/buy/response_objects/order.dart @@ -0,0 +1,17 @@ +import 'package:stackwallet/models/buy/response_objects/quote.dart'; + +class SimplexOrder { + final SimplexQuote quote; + + late final String paymentId; + late final String orderId; + late final String userId; + // TODO remove after userIds are sourced from isar/storage + + SimplexOrder({ + required this.quote, + required this.paymentId, + required this.orderId, + required this.userId, + }); +} diff --git a/lib/models/buy/response_objects/quote.dart b/lib/models/buy/response_objects/quote.dart new file mode 100644 index 000000000..e91ca4d67 --- /dev/null +++ b/lib/models/buy/response_objects/quote.dart @@ -0,0 +1,26 @@ +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/models/buy/response_objects/crypto.dart'; +import 'package:stackwallet/models/buy/response_objects/fiat.dart'; + +class SimplexQuote { + final Crypto crypto; + final Fiat fiat; + + late final Decimal youPayFiatPrice; + late final Decimal youReceiveCryptoAmount; + + late final String id; + late final String receivingAddress; + + late final bool buyWithFiat; + + SimplexQuote({ + required this.crypto, + required this.fiat, + required this.youPayFiatPrice, + required this.youReceiveCryptoAmount, + required this.id, + required this.receivingAddress, + required this.buyWithFiat, + }); +} diff --git a/lib/models/buy/simplex/simplex.dart b/lib/models/buy/simplex/simplex.dart new file mode 100644 index 000000000..bb75036ab --- /dev/null +++ b/lib/models/buy/simplex/simplex.dart @@ -0,0 +1,51 @@ +import 'package:decimal/decimal.dart'; +import 'package:stackwallet/models/buy/response_objects/crypto.dart'; +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'; + +class Simplex { + List supportedCryptos = []; + List supportedFiats = []; + SimplexQuote quote = SimplexQuote( + crypto: Crypto.fromJson({'ticker': 'BTC', 'name': 'Bitcoin', 'image': ''}), + fiat: Fiat.fromJson( + {'ticker': 'USD', 'name': 'United States Dollar', 'image': ''}), + youPayFiatPrice: Decimal.parse("100"), + youReceiveCryptoAmount: Decimal.parse("1.0238917"), + id: "someID", + receivingAddress: '', + buyWithFiat: true, + ); + SimplexOrder order = SimplexOrder( + quote: SimplexQuote( + crypto: + Crypto.fromJson({'ticker': 'BTC', 'name': 'Bitcoin', 'image': ''}), + fiat: Fiat.fromJson( + {'ticker': 'USD', 'name': 'United States Dollar', 'image': ''}), + youPayFiatPrice: Decimal.parse("100"), + youReceiveCryptoAmount: Decimal.parse("1.0238917"), + id: "someID", + receivingAddress: '', + buyWithFiat: true, + ), + orderId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + paymentId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', + userId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'); + + void updateSupportedCryptos(List newCryptos) { + supportedCryptos = newCryptos; + } + + void updateSupportedFiats(List newFiats) { + supportedFiats = newFiats; + } + + void updateQuote(SimplexQuote newQuote) { + quote = newQuote; + } + + void updateOrder(SimplexOrder newOrder) { + order = newOrder; + } +} diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart new file mode 100644 index 000000000..83b47e136 --- /dev/null +++ b/lib/pages/buy_view/buy_form.dart @@ -0,0 +1,1185 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:intl/intl.dart'; +import 'package:stackwallet/models/buy/response_objects/crypto.dart'; +import 'package:stackwallet/models/buy/response_objects/fiat.dart'; +import 'package:stackwallet/models/buy/response_objects/quote.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/pages/buy_view/buy_quote_preview.dart'; +import 'package:stackwallet/pages/buy_view/sub_widgets/crypto_selection_view.dart'; +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/simplex/simplex_api.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/custom_loading_overlay.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class BuyForm extends ConsumerStatefulWidget { + const BuyForm({ + Key? key, + this.clipboard = const ClipboardWrapper(), + this.scanner = const BarcodeScannerWrapper(), + }) : super(key: key); + + final ClipboardInterface clipboard; + final BarcodeScannerInterface scanner; + + @override + ConsumerState createState() => _BuyFormState(); +} + +class _BuyFormState extends ConsumerState { + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + + late final TextEditingController _receiveAddressController; + late final TextEditingController _buyAmountController; + final FocusNode _receiveAddressFocusNode = FocusNode(); + final FocusNode _fiatFocusNode = FocusNode(); + final FocusNode _cryptoFocusNode = FocusNode(); + final FocusNode _buyAmountFocusNode = FocusNode(); + + final isDesktop = Util.isDesktop; + + List? coins; + List? fiats; + String? _address; + + Fiat? selectedFiat; + Crypto? selectedCrypto; + SimplexQuote quote = SimplexQuote( + 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", + receivingAddress: '', + buyWithFiat: true, + ); // TODO enum this or something + + bool buyWithFiat = true; + bool _addressToggleFlag = false; + bool _hovering1 = false; + bool _hovering2 = false; + + void fiatFieldOnChanged(String value) async {} + + void cryptoFieldOnChanged(String value) async {} + + void selectCrypto() async { + if (ref.read(simplexProvider).supportedCryptos.isEmpty) { + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading currency data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), + ), + ); + await _loadSimplexCryptos(); + shouldPop = true; + if (mounted) { + Navigator.of(context, rootNavigator: isDesktop).pop(); + } + } + + await _showFloatingCryptoSelectionSheet( + coins: ref.read(simplexProvider).supportedCryptos, + onSelected: (crypto) { + setState(() { + selectedCrypto = crypto; + }); + }, + ); + } + + Future _showFloatingCryptoSelectionSheet({ + required List coins, + required void Function(Crypto) onSelected, + }) async { + _fiatFocusNode.unfocus(); + _cryptoFocusNode.unfocus(); + + final result = isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Choose a crypto to buy", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension()! + .background, + child: CryptoSelectionView( + coins: coins, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CryptoSelectionView( + coins: coins, + ), + ), + ); + + if (mounted && result is Crypto) { + onSelected(result); + } + } + + Future selectFiat() async { + if (ref.read(simplexProvider).supportedFiats.isEmpty) { + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading currency data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), + ), + ); + await _loadSimplexFiats(); + shouldPop = true; + if (mounted) { + Navigator.of(context, rootNavigator: isDesktop).pop(); + } + } + + await _showFloatingFiatSelectionSheet( + fiats: ref.read(simplexProvider).supportedFiats, + onSelected: (fiat) { + setState(() { + selectedFiat = fiat; + }); + }, + ); + } + + Future _loadSimplexCryptos() async { + final response = await SimplexAPI.instance.getSupportedCryptos(); + + if (response.value != null) { + ref + .read(simplexProvider) + .updateSupportedCryptos(response.value!); // TODO validate + } else { + Logging.instance.log( + "_loadSimplexCurrencies: $response", + level: LogLevel.Warning, + ); + } + } + + Future _loadSimplexFiats() async { + final response = await SimplexAPI.instance.getSupportedFiats(); + + if (response.value != null) { + ref + .read(simplexProvider) + .updateSupportedFiats(response.value!); // TODO validate + } else { + Logging.instance.log( + "_loadSimplexCurrencies: $response", + level: LogLevel.Warning, + ); + } + } + + Future _showFloatingFiatSelectionSheet({ + required List fiats, + required void Function(Fiat) onSelected, + }) async { + _fiatFocusNode.unfocus(); + _cryptoFocusNode.unfocus(); + + final result = isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Choose a fiat with which to pay", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension()! + .background, + child: FiatSelectionView( + fiats: fiats, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => FiatSelectionView( + fiats: fiats, + ), + ), + ); + + if (mounted && result is Fiat) { + onSelected(result); + } + } + + String? _fetchIconUrlFromTicker(String? ticker) { + if (ticker == null) return null; + + return null; + } + + bool isStackCoin(String? ticker) { + if (ticker == null) return false; + + try { + coinFromTickerCaseInsensitive(ticker); + return true; + } on ArgumentError catch (_) { + return false; + } + } + + Widget? getIconForTicker(String 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; + } + + Future previewQuote(SimplexQuote quote) async { + // if (ref.read(simplexProvider).quote.id == "someID") { + // // TODO make a better way of detecting a default SimplexQuote + bool shouldPop = false; + unawaited( + showDialog( + context: context, + builder: (context) => WillPopScope( + child: const CustomLoadingOverlay( + message: "Loading quote data", + eventBus: null, + ), + onWillPop: () async => shouldPop, + ), + ), + ); + + quote = SimplexQuote( + crypto: selectedCrypto!, + fiat: selectedFiat!, + youPayFiatPrice: buyWithFiat + ? Decimal.parse(_buyAmountController.text) + : Decimal.parse("100"), // dummy value + youReceiveCryptoAmount: buyWithFiat + ? Decimal.parse("0.000420282") // dummy value + : Decimal.parse(_buyAmountController.text), // Ternary for this + id: "id", // anything; we get an ID back + receivingAddress: _receiveAddressController.text, + buyWithFiat: buyWithFiat, + ); + + await _loadQuote(quote); + shouldPop = true; + if (mounted) { + Navigator.of(context, rootNavigator: isDesktop).pop(); + } + // } + + await _showFloatingBuyQuotePreviewSheet( + quote: ref.read(simplexProvider).quote, + onSelected: (quote) { + // setState(() { + // selectedFiat = fiat; + // }); + // TODO launch URL + }, + ); + } + + Future _loadQuote(SimplexQuote quote) async { + final response = await SimplexAPI.instance.getQuote(quote); + + if (response.value != null) { + ref.read(simplexProvider).updateQuote(response.value!); + } else { + Logging.instance.log( + "_loadQuote: $response", + level: LogLevel.Warning, + ); + } + } + + Future _showFloatingBuyQuotePreviewSheet({ + required SimplexQuote quote, + required void Function(SimplexQuote) onSelected, + }) async { + _fiatFocusNode.unfocus(); + _cryptoFocusNode.unfocus(); + + final result = isDesktop + ? await showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Preview quote", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension()! + .background, + child: BuyQuotePreviewView( + quote: quote, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => BuyQuotePreviewView( + quote: quote, + ), + ), + ); + + if (mounted && result is SimplexQuote) { + onSelected(result); + } + } + + @override + void initState() { + _receiveAddressController = TextEditingController(); + _buyAmountController = TextEditingController(); + + clipboard = widget.clipboard; + scanner = widget.scanner; + + coins = ref.read(simplexProvider).supportedCryptos; + fiats = ref.read(simplexProvider).supportedFiats; + // 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': ''}), + youPayFiatPrice: Decimal.parse("100"), + youReceiveCryptoAmount: Decimal.parse("1.0238917"), + id: "someID", + receivingAddress: '', + buyWithFiat: true, + ); // 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': ''}); + + // TODO set initial crypto to open wallet if a wallet is open + + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + Locale locale = Localizations.localeOf(context); + var format = NumberFormat.simpleCurrency(locale: locale.toString()); + // See https://stackoverflow.com/a/67055685 + + return ConditionalParent( + condition: isDesktop, + builder: (child) => SizedBox( + width: 458, + child: child, + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: IntrinsicHeight( + child: child, + ), + ), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "I want to buy", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + SizedBox( + height: isDesktop ? 10 : 4, + ), + MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _hovering1 = true), + onExit: (_) => setState(() => _hovering1 = false), + child: GestureDetector( + onTap: () { + selectCrypto(); + }, + child: RoundedContainer( + padding: + const EdgeInsets.symmetric(vertical: 6, horizontal: 2), + color: _hovering1 + ? Theme.of(context) + .extension()! + .highlight + .withOpacity(_hovering1 ? 0.3 : 0) + : Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + getIconForTicker(selectedCrypto?.ticker ?? "BTC") + as Widget, + const SizedBox( + width: 10, + ), + Expanded( + child: Text( + selectedCrypto?.ticker ?? "ERR", + style: STextStyles.largeMedium14(context), + ), + ), + SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, + width: 10, + height: 5, + ), + ], + ), + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 20 : 12, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "I want to pay with", + style: STextStyles.itemSubtitle(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + ], + ), + SizedBox( + height: isDesktop ? 10 : 4, + ), + MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _hovering2 = true), + onExit: (_) => setState(() => _hovering2 = false), + child: GestureDetector( + onTap: () { + selectFiat(); + }, + child: RoundedContainer( + padding: + const EdgeInsets.symmetric(vertical: 3, horizontal: 2), + color: _hovering2 + ? Theme.of(context) + .extension()! + .highlight + .withOpacity(_hovering2 ? 0.3 : 0) + : Theme.of(context) + .extension()! + .textFieldDefaultBG, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, top: 12.0, right: 12.0, bottom: 12.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 3, horizontal: 6), + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .highlight, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + format.simpleCurrencySymbol( + selectedFiat?.ticker ?? "ERR".toUpperCase()), + textAlign: TextAlign.center, + style: STextStyles.smallMed12(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + const SizedBox( + width: 8, + ), + Text( + "${selectedFiat?.ticker ?? 'ERR'}", + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Text( + "${selectedFiat?.name ?? 'Error'}", + style: STextStyles.largeMedium14(context), + ), + ), + SvgPicture.asset( + Assets.svg.chevronDown, + color: Theme.of(context) + .extension()! + .buttonTextSecondaryDisabled, + width: 10, + height: 5, + ), + ], + ), + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 4, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + buyWithFiat ? "Enter amount" : "Enter crypto amount", + style: STextStyles.itemSubtitle(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + BlueTextButton( + text: buyWithFiat ? "Use crypto amount" : "Use fiat amount", + onTap: () { + setState(() { + buyWithFiat = !buyWithFiat; + }); + }, + ) + ], + ), + SizedBox( + height: isDesktop ? 10 : 4, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + key: const Key("amountInputFieldCryptoTextFieldKey"), + controller: _buyAmountController, + focusNode: _buyAmountFocusNode, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.left, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places or + // 2 if fiat + TextInputFormatter.withFunction( + (oldValue, newValue) { + final regexString = 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; + }, + ), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + // top: 22, + // right: 12, + // bottom: 22, + left: 0, + top: 8, + bottom: 10, + right: 5, + ), + hintText: "0", + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldDefaultText, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Row(children: [ + const SizedBox(width: 2), + buyWithFiat + ? Container( + padding: const EdgeInsets.symmetric( + vertical: 3, horizontal: 6), + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .highlight, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + format.simpleCurrencySymbol( + selectedFiat?.ticker ?? + "ERR".toUpperCase()), + textAlign: TextAlign.center, + style: STextStyles.smallMed12(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ) + : getIconForTicker(selectedCrypto?.ticker ?? "BTC") + as Widget, + SizedBox( + width: buyWithFiat + ? 8 + : 10), // maybe make isDesktop-aware? + Text( + buyWithFiat + ? selectedFiat?.ticker ?? "ERR" + : selectedCrypto?.ticker ?? "ERR", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ]), + ), + ), + suffixIcon: Padding( + padding: const EdgeInsets.all(0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buyAmountController.text.isNotEmpty + ? TextFieldIconButton( + key: const Key( + "buyViewClearAddressFieldButtonKey"), + onTap: () { + _buyAmountController.text = ""; + // _receiveAddress = ""; + setState(() {}); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "buyViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + + final amountString = + Decimal.tryParse(data?.text ?? ""); + if (amountString != null) { + _buyAmountController.text = + amountString.toString(); + + setState(() {}); + } + }, + child: _buyAmountController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + ], + ), + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 20 : 12, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Enter receiving address", + style: STextStyles.itemSubtitle(context).copyWith( + color: + Theme.of(context).extension()!.textDark3, + ), + ), + if (isStackCoin(selectedCrypto?.ticker)) + BlueTextButton( + text: "Choose from stack", + onTap: () { + try { + final coin = coinFromTickerCaseInsensitive( + selectedCrypto!.ticker, + ); + Navigator.of(context) + .pushNamed( + ChooseFromStackView.routeName, + arguments: coin, + ) + .then((value) async { + if (value is String) { + final manager = ref + .read(walletsChangeNotifierProvider) + .getManager(value); + + // _toController.text = manager.walletName; + // model.recipientAddress = + // await manager.currentReceivingAddress; + _receiveAddressController.text = + await manager.currentReceivingAddress; + + setState(() {}); + } + }); + } catch (e, s) { + Logging.instance.log("$e\n$s", level: LogLevel.Info); + } + }, + ), + ], + ), + SizedBox( + height: isDesktop ? 10 : 4, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("buyViewReceiveAddressFieldKey"), + controller: _receiveAddressController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue; + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _receiveAddressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${selectedCrypto?.ticker} address", + _receiveAddressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 13, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: _receiveAddressController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "buyViewClearAddressFieldButtonKey"), + onTap: () { + _receiveAddressController.text = ""; + _address = ""; + setState(() { + _addressToggleFlag = true; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "buyViewPasteAddressFieldButtonKey"), + onTap: () async { + final ClipboardData? data = await clipboard + .getData(Clipboard.kTextPlain); + if (data?.text != null && + data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring( + 0, content.indexOf("\n")); + } + + _receiveAddressController.text = content; + _address = content; + + setState(() { + _addressToggleFlag = + _receiveAddressController + .text.isNotEmpty; + }); + } + }, + child: _receiveAddressController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (_receiveAddressController.text.isEmpty && + isStackCoin(selectedCrypto?.ticker) && + isDesktop) + TextFieldIconButton( + key: const Key("buyViewAddressBookButtonKey"), + onTap: () async { + final entry = + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: STextStyles.desktopH3( + context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coinFromTickerCaseInsensitive( + selectedCrypto!.ticker + .toString()), + ), + ), + ], + ), + ), + ); + + if (entry != null) { + _receiveAddressController.text = + entry.address; + _address = entry.address; + + setState(() { + _addressToggleFlag = true; + }); + } + }, + child: const AddressBookIcon(), + ), + if (_receiveAddressController.text.isEmpty && + isStackCoin(selectedCrypto?.ticker) && + !isDesktop) + TextFieldIconButton( + key: const Key("buyViewAddressBookButtonKey"), + onTap: () { + Navigator.of(context, rootNavigator: isDesktop) + .pushNamed( + AddressBookView.routeName, + ); + }, + child: const AddressBookIcon(), + ), + if (_receiveAddressController.text.isEmpty && + !isDesktop) + TextFieldIconButton( + key: const Key("buyViewScanQrButtonKey"), + onTap: () async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + Logging.instance.log( + "qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = AddressUtils.parseUri( + qrResult.rawContent); + + Logging.instance.log( + "qrResult parsed: $results", + level: LogLevel.Info); + + if (results.isNotEmpty) { + // auto fill address + _address = results["address"] ?? ""; + _receiveAddressController.text = _address!; + + setState(() { + _addressToggleFlag = + _receiveAddressController + .text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else { + _address = qrResult.rawContent; + _receiveAddressController.text = + _address ?? ""; + + setState(() { + _addressToggleFlag = + _receiveAddressController + .text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning, + ); + } + }, + child: const QrCodeIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 20 : 12, + ), + MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { + if (_receiveAddressController.text.isNotEmpty && + _buyAmountController.text.isNotEmpty) { + previewQuote(quote); + } + }, + child: PrimaryButton( + buttonHeight: isDesktop ? ButtonHeight.l : null, + enabled: _receiveAddressController.text.isNotEmpty && + _buyAmountController.text.isNotEmpty, + onPressed: () { + if (_receiveAddressController.text.isNotEmpty && + _buyAmountController.text.isNotEmpty) { + previewQuote(quote); + } + }, + label: "Preview quote", + )), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/buy_view/buy_order_details.dart b/lib/pages/buy_view/buy_order_details.dart new file mode 100644 index 000000000..2b584578e --- /dev/null +++ b/lib/pages/buy_view/buy_order_details.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/buy/response_objects/order.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class BuyOrderDetailsView extends StatefulWidget { + const BuyOrderDetailsView({ + Key? key, + required this.order, + }) : super(key: key); + + final SimplexOrder order; + + static const String routeName = "/buyOrderDetails"; + + @override + State createState() => _BuyOrderDetailsViewState(); +} + +class _BuyOrderDetailsViewState extends State { + final isDesktop = Util.isDesktop; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.backgroundAppBar, + leading: const AppBarBackButton(), + title: Text( + "Order details", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Simplex order", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Purchase ID", + style: STextStyles.label(context), + ), + Text( + widget.order.paymentId, + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "User ID", + style: STextStyles.label(context), + ), + Text( + widget.order.userId, + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Quote ID", + style: STextStyles.label(context), + ), + Text( + widget.order.quote.id, + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Quoted cost", + style: STextStyles.label(context), + ), + Text( + "${widget.order.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.order.quote.fiat.ticker.toUpperCase()}", + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + // RoundedWhiteContainer( + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "You pay with", + // style: STextStyles.label(context), + // ), + // Text( + // widget.quote.fiat.name, + // style: STextStyles.label(context).copyWith( + // color: Theme.of(context).extension()!.textDark, + // ), + // ), + // ], + // ), + // ), + // const SizedBox( + // height: 8, + // ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Quoted amount", + style: STextStyles.label(context), + ), + Text( + "${widget.order.quote.youReceiveCryptoAmount} ${widget.order.quote.crypto.ticker.toUpperCase()}", + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Receiving ${widget.order.quote.crypto.ticker.toUpperCase()} address", + style: STextStyles.label(context), + ), + Text( + "${widget.order.quote.receivingAddress} ", + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Provider", + style: STextStyles.label(context), + ), + SizedBox( + width: 64, + height: 32, + child: SvgPicture.asset( + Assets.buy.simplexLogo(context), + ), + ), + ], + ), + ), + const SizedBox( + height: 24, + ), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Text( + "This information is not saved,\nscreenshot it now for your records", + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + textAlign: TextAlign.center, + ), + ]), + const Spacer(), + PrimaryButton( + label: "Dismiss", + onPressed: () { + Navigator.of(context, rootNavigator: isDesktop).pop(); + }, + ) + ], + ), + ); + } +} diff --git a/lib/pages/buy_view/buy_quote_preview.dart b/lib/pages/buy_view/buy_quote_preview.dart new file mode 100644 index 000000000..3526fed8e --- /dev/null +++ b/lib/pages/buy_view/buy_quote_preview.dart @@ -0,0 +1,233 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:intl/intl.dart'; +import 'package:stackwallet/models/buy/response_objects/quote.dart'; +import 'package:stackwallet/pages/buy_view/sub_widgets/buy_warning_popup.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class BuyQuotePreviewView extends StatefulWidget { + const BuyQuotePreviewView({ + Key? key, + required this.quote, + }) : super(key: key); + + final SimplexQuote quote; + + static const String routeName = "/buyQuotePreview"; + + @override + State createState() => _BuyQuotePreviewViewState(); +} + +class _BuyQuotePreviewViewState extends State { + final isDesktop = Util.isDesktop; + + Future _buyWarning() async { + await showDialog( + context: context, + builder: (context) => BuyWarningPopup( + quote: widget.quote, + ), + ); + } + + @override + Widget build(BuildContext context) { + Locale locale = Localizations.localeOf(context); + var format = NumberFormat.simpleCurrency(locale: locale.toString()); + // See https://stackoverflow.com/a/67055685 + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.backgroundAppBar, + leading: const AppBarBackButton(), + title: Text( + "Preview quote", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.all(4), + child: child, + ), + ), + ), + ), + ); + }, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "Buy ${widget.quote.crypto.ticker.toUpperCase()}", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 16, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "You pay", + style: STextStyles.label(context), + ), + Text( + "${format.simpleCurrencySymbol(widget.quote.fiat.ticker.toUpperCase())}${widget.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.quote.fiat.ticker.toUpperCase()}", + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + // RoundedWhiteContainer( + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text( + // "You pay with", + // style: STextStyles.label(context), + // ), + // Text( + // widget.quote.fiat.name, + // style: STextStyles.label(context).copyWith( + // color: Theme.of(context).extension()!.textDark, + // ), + // ), + // ], + // ), + // ), + // const SizedBox( + // height: 8, + // ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "You receive", + style: STextStyles.label(context), + ), + Text( + "${widget.quote.youReceiveCryptoAmount} ${widget.quote.crypto.ticker.toUpperCase()}", + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Receiving ${widget.quote.crypto.ticker.toUpperCase()} address", + style: STextStyles.label(context), + ), + Text( + "${widget.quote.receivingAddress} ", + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Quote ID", + style: STextStyles.label(context), + ), + Text( + widget.quote.id, + style: STextStyles.label(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + RoundedWhiteContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Provider", + style: STextStyles.label(context), + ), + SizedBox( + width: 64, + height: 32, + child: SvgPicture.asset( + Assets.buy.simplexLogo(context), + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + const Spacer(), + PrimaryButton( + label: "Buy", + onPressed: _buyWarning, + ) + ], + ), + ); + } +} diff --git a/lib/pages/buy_view/buy_view.dart b/lib/pages/buy_view/buy_view.dart index cc536dd45..0eb44e87e 100644 --- a/lib/pages/buy_view/buy_view.dart +++ b/lib/pages/buy_view/buy_view.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/pages/buy_view/buy_form.dart'; class BuyView extends StatefulWidget { const BuyView({Key? key}) : super(key: key); + static const String routeName = "/stackBuyView"; + @override State createState() => _BuyViewState(); } @@ -11,39 +13,16 @@ class BuyView extends StatefulWidget { class _BuyViewState extends State { @override Widget build(BuildContext context) { - //todo: check if print needed - // debugPrint("BUILD: BuyView"); - return SafeArea( - child: Center( - child: SingleChildScrollView( - child: Column( - children: [ - Center( - child: Text( - "Coming soon", - style: STextStyles.pageTitleH1(context), - ), - ), - ], - ), + debugPrint("BUILD: $runtimeType"); + + return const SafeArea( + child: Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, ), - // child: Column( - // children: [ - // Container( - // color: Colors.green, - // child: Text("BuyView"), - // ), - // Container( - // color: Colors.green, - // child: Text("BuyView"), - // ), - // Spacer(), - // Container( - // color: Colors.green, - // child: Text("BuyView"), - // ), - // ], - // ), + child: BuyForm(), ), ); } diff --git a/lib/pages/buy_view/sub_widgets/buy_warning_popup.dart b/lib/pages/buy_view/sub_widgets/buy_warning_popup.dart new file mode 100644 index 000000000..082a19306 --- /dev/null +++ b/lib/pages/buy_view/sub_widgets/buy_warning_popup.dart @@ -0,0 +1,143 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/buy/response_objects/order.dart'; +import 'package:stackwallet/models/buy/response_objects/quote.dart'; +import 'package:stackwallet/pages/buy_view/buy_order_details.dart'; +import 'package:stackwallet/services/buy/buy_response.dart'; +import 'package:stackwallet/services/buy/simplex/simplex_api.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class BuyWarningPopup extends StatelessWidget { + BuyWarningPopup({ + Key? key, + required this.quote, + this.order, + }) : super(key: key); + + final SimplexQuote quote; + SimplexOrder? order; + + Future> newOrder(SimplexQuote quote) async { + final response = 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; + } + + Future> redirect(SimplexOrder order) async { + return SimplexAPI.instance.redirect(order); + } + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + + Future _buyInvoice() async { + await showDialog( + context: context, + // useRootNavigator: isDesktop, + builder: (context) { + return isDesktop + ? DesktopDialog( + maxHeight: 700, + maxWidth: 580, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Order details", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: Row( + children: [ + Expanded( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + borderColor: Theme.of(context) + .extension()! + .background, + child: BuyOrderDetailsView( + order: order as SimplexOrder, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ) + : BuyOrderDetailsView( + order: order as SimplexOrder, + ); + }); + } + + return StackDialog( + title: "Buy ${quote.crypto.ticker}", + message: "This purchase is provided and fulfilled by Simplex by nuvei " + "(a third party). You will be taken to their website. Please follow " + "their instructions.", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context, rootNavigator: isDesktop).pop, + ), + 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(); + }); + }, + ), + icon: SizedBox( + width: 64, + height: 32, + child: SvgPicture.asset( + Assets.buy.simplexLogo(context), + ), + ), + ); + } +} diff --git a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart new file mode 100644 index 000000000..2d0fe3095 --- /dev/null +++ b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart @@ -0,0 +1,262 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/buy/response_objects/crypto.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.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/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class CryptoSelectionView extends StatefulWidget { + const CryptoSelectionView({ + Key? key, + required this.coins, + }) : super(key: key); + + final List coins; + + @override + State createState() => _CryptoSelectionViewState(); +} + +class _CryptoSelectionViewState extends State { + late TextEditingController _searchController; + final _searchFocusNode = FocusNode(); + + late final List coins; + late List _coins; + + void filter(String text) { + setState(() { + _coins = [ + ...coins.where((e) => + e.name.toLowerCase().contains(text.toLowerCase()) || + e.ticker.toLowerCase().contains(text.toLowerCase())) + ]; + }); + } + + @override + void initState() { + _searchController = TextEditingController(); + + coins = [...widget.coins]; + coins.sort( + (a, b) => a.ticker.toLowerCase().compareTo(b.ticker.toLowerCase())); + for (Coin coin in Coin.values.reversed) { + int index = coins.indexWhere((element) => + element.ticker.toLowerCase() == coin.ticker.toLowerCase()); + if (index > 0) { + final currency = coins.removeAt(index); + coins.insert(0, currency); + } + } + + _coins = [...coins]; + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isDesktop = Util.isDesktop; + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a coin to buy", + style: STextStyles.pageTitleH2(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: filter, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + filter(""); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 10, + ), + Text( + "All coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _coins.length, + itemBuilder: (builderContext, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(_coins[index]); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: getIconForTicker(_coins[index].ticker)), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _coins[index].name, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + _coins[index].ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} + +bool isStackCoin(String? ticker) { + if (ticker == null) return false; + + try { + coinFromTickerCaseInsensitive(ticker); + return true; + } on ArgumentError catch (_) { + return false; + } +} + +Widget? getIconForTicker(String 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 new file mode 100644 index 000000000..e777ab96c --- /dev/null +++ b/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart @@ -0,0 +1,270 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:intl/intl.dart'; +import 'package:stackwallet/models/buy/response_objects/fiat.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/fiat_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class FiatSelectionView extends StatefulWidget { + const FiatSelectionView({ + Key? key, + required this.fiats, + }) : super(key: key); + + final List fiats; + + @override + State createState() => _FiatSelectionViewState(); +} + +class _FiatSelectionViewState extends State { + late TextEditingController _searchController; + final _searchFocusNode = FocusNode(); + + late final List fiats; + late List _fiats; + + void filter(String text) { + setState(() { + _fiats = [ + ...fiats.where((e) => + e.name.toLowerCase().contains(text.toLowerCase()) || + e.ticker.toLowerCase().contains(text.toLowerCase())) + ]; + }); + } + + @override + void initState() { + _searchController = TextEditingController(); + + fiats = [...widget.fiats]; + fiats.sort( + (a, b) => a.ticker.toLowerCase().compareTo(b.ticker.toLowerCase())); + for (Fiats fiat in Fiats.values.reversed) { + int index = fiats.indexWhere((element) => + element.ticker.toLowerCase() == fiat.ticker.toLowerCase()); + if (index > 0) { + final currency = fiats.removeAt(index); + fiats.insert(0, currency); + } + } + + _fiats = [...fiats]; + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Locale locale = Localizations.localeOf(context); + var format = NumberFormat.simpleCurrency(locale: locale.toString()); + // See https://stackoverflow.com/a/67055685 + + final isDesktop = Util.isDesktop; + + return ConditionalParent( + condition: !isDesktop, + builder: (child) { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Choose a currency with which to pay", + style: STextStyles.pageTitleH2(context), + ), + ), + body: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), + child: child, + ), + ), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: isDesktop ? MainAxisSize.min : MainAxisSize.max, + children: [ + if (!isDesktop) + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: _searchFocusNode, + onChanged: filter, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + }); + filter(""); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 10, + ), + Text( + "All currencies", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: _fiats.length, + itemBuilder: (builderContext, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(_fiats[index]); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(7.5), + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .highlight, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + format.simpleCurrencySymbol( + _fiats[index].ticker.toUpperCase()), + style: STextStyles.subtitle(context).apply( + fontSizeFactor: (1 / + format + .simpleCurrencySymbol(_fiats[index] + .ticker + .toUpperCase()) + .length * // Couldn't get pow() working here + format + .simpleCurrencySymbol(_fiats[index] + .ticker + .toUpperCase()) + .length)), + textAlign: TextAlign.center, + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _fiats[index].name, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + _fiats[index].ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 2abe0d347..808a0417f 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -61,6 +61,8 @@ class _ConfirmChangeNowSendViewState late final String routeOnSuccessName; late final Trade trade; + final isDesktop = Util.isDesktop; + Future _attemptSend(BuildContext context) async { unawaited( showDialog( @@ -227,8 +229,6 @@ class _ConfirmChangeNowSendViewState final managerProvider = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManagerProvider(walletId))); - final isDesktop = Util.isDesktop; - return ConditionalParent( condition: !isDesktop, builder: (child) { @@ -238,7 +238,7 @@ class _ConfirmChangeNowSendViewState Theme.of(context).extension()!.background, appBar: AppBar( backgroundColor: - Theme.of(context).extension()!.background, + Theme.of(context).extension()!.backgroundAppBar, leading: AppBarBackButton( onPressed: () async { // if (FocusScope.of(context).hasFocus) { diff --git a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart index 284d4eddd..8c82bbbc4 100644 --- a/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart +++ b/lib/pages/exchange_view/sub_widgets/rate_type_toggle.dart @@ -3,12 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/toggle.dart'; -import '../../../utilities/constants.dart'; - class RateTypeToggle extends ConsumerWidget { const RateTypeToggle({ Key? key, @@ -37,10 +36,16 @@ class RateTypeToggle extends ConsumerWidget { } }, isOn: !estimated, - onColor: Theme.of(context).extension()!.textFieldDefaultBG, + onColor: isDesktop + ? Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOn + : Theme.of(context).extension()!.rateTypeToggleColorOn, offColor: isDesktop - ? Theme.of(context).extension()!.buttonBackSecondary - : Theme.of(context).extension()!.popupBG, + ? Theme.of(context) + .extension()! + .rateTypeToggleDesktopColorOff + : Theme.of(context).extension()!.rateTypeToggleColorOff, decoration: BoxDecoration( borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 49317193e..5a09203cb 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/buy_view/buy_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_loading_overlay.dart'; import 'package:stackwallet/pages/exchange_view/exchange_view.dart'; import 'package:stackwallet/pages/home_view/sub_widgets/home_view_button_bar.dart'; @@ -45,6 +46,7 @@ class _HomeViewState extends ConsumerState { bool _exitEnabled = false; final _exchangeDataLoadingService = ExchangeDataLoadingService(); + // final _buyDataLoadingService = BuyDataLoadingService(); Future _onWillPop() async { // go to home view when tapping back on the main exchange view @@ -92,6 +94,26 @@ class _HomeViewState extends ConsumerState { } } + // void _loadSimplexData() { + // // unawaited future + // if (ref.read(prefsChangeNotifierProvider).externalCalls) { + // _buyDataLoadingService.loadAll(ref); + // } else { + // Logging.instance.log("User does not want to use external calls", + // level: LogLevel.Info); + // } + // } + + bool _lock = false; + + Future _animateToPage(int index) async { + await _pageController.animateToPage( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.decelerate, + ); + } + @override void initState() { _pageController = PageController(); @@ -106,7 +128,14 @@ class _HomeViewState extends ConsumerState { ), ], ), - // const BuyView(), + if (Constants.enableBuy) + // Stack( + // children: [ + const BuyView(), + // BuyLoadingOverlayView( + // unawaitedLoad: _loadSimplexData, + // ), + // ], ]; ref.read(notificationsProvider).startCheckingWatchedNotifications(); @@ -301,35 +330,31 @@ class _HomeViewState extends ConsumerState { builder: (_, _ref, __) { _ref.listen(homeViewPageIndexStateProvider, (previous, next) { - if (next is int) { + if (next is int && next >= 0 && next <= 2) { if (next == 1) { _exchangeDataLoadingService.loadAll(ref); } - if (next >= 0 && next <= 1) { - _pageController.animateToPage( - next, - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - ); - } + // if (next == 2) { + // _buyDataLoadingService.loadAll(ref); + // } + + _lock = true; + _animateToPage(next).then((value) => _lock = false); } }); return PageView( controller: _pageController, children: _children, onPageChanged: (pageIndex) { - ref.read(homeViewPageIndexStateProvider.state).state = - pageIndex; + if (!_lock) { + ref.read(homeViewPageIndexStateProvider.state).state = + pageIndex; + } }, ); }, ), ), - // Expanded( - // child: HomeStack( - // children: _children, - // ), - // ), ], ), ), diff --git a/lib/pages/home_view/sub_widgets/home_view_button_bar.dart b/lib/pages/home_view/sub_widgets/home_view_button_bar.dart index 79328a29e..2992be514 100644 --- a/lib/pages/home_view/sub_widgets/home_view_button_bar.dart +++ b/lib/pages/home_view/sub_widgets/home_view_button_bar.dart @@ -127,37 +127,48 @@ class _HomeViewButtonBarState extends ConsumerState { ), ), ), - // TODO: Do not delete this code. - // only temporarily disabled - // SizedBox( - // width: 8, - // ), - // Expanded( - // child: TextButton( - // style: ButtonStyle( - // minimumSize: MaterialStateProperty.all(Size(46, 36)), - // backgroundColor: MaterialStateProperty.all( - // selectedIndex == 2 - // ? CFColors.stackAccent - // : CFColors.disabledButton, - // ), - // ), - // onPressed: () { - // FocusScope.of(context).unfocus(); - // if (selectedIndex != 2) { - // ref.read(homeViewPageIndexStateProvider.state).state = 2; - // } - // }, - // child: Text( - // "Buy", - // style: STextStyles.button(context).copyWith( - // fontSize: 14, - // color: - // selectedIndex == 2 ? CFColors.light1 : Theme.of(context).extension()!.accentColorDark - // ), - // ), - // ), - // ), + const SizedBox( + width: 8, + ), + Expanded( + child: TextButton( + style: selectedIndex == 2 + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context)! + .copyWith( + minimumSize: + MaterialStateProperty.all(const Size(46, 36)), + ) + : Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context)! + .copyWith( + minimumSize: + MaterialStateProperty.all(const Size(46, 36)), + ), + onPressed: () async { + FocusScope.of(context).unfocus(); + if (selectedIndex != 2) { + ref.read(homeViewPageIndexStateProvider.state).state = 2; + } + // await BuyDataLoadingService().loadAll(ref); + }, + child: Text( + "Buy", + style: STextStyles.button(context).copyWith( + fontSize: 14, + color: selectedIndex == 2 + ? Theme.of(context) + .extension()! + .buttonTextPrimary + : Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + ), + ), + ), ], ); } diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index 1a5e23e98..e120d238b 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -232,6 +232,8 @@ class _StackPrivacyCalls extends ConsumerState { if (isEasy) { unawaited(ExchangeDataLoadingService() .loadAll(ref)); + // unawaited( + // BuyDataLoadingService().loadAll(ref)); ref .read(priceAnd24hChangeNotifierProvider) .start(true); 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 d8ca42192..c663b8ba0 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -416,39 +416,47 @@ class _WalletNavigationBarState extends ConsumerState { const SizedBox( width: 12, ), - // TODO: Do not delete this code. - // only temporarily disabled - // Spacer( - // flex: 2, - // ), - // GestureDetector( - // onTap: onBuyPressed, - // child: Container( - // color: Colors.transparent, - // child: Padding( - // padding: const EdgeInsets.symmetric(vertical: 2.0), - // child: Column( - // crossAxisAlignment: CrossAxisAlignment.center, - // children: [ - // Spacer(), - // SvgPicture.asset( - // Assets.svg.buy, - // width: 24, - // height: 24, - // ), - // SizedBox( - // height: 4, - // ), - // Text( - // "Buy", - // style: STextStyles.buttonSmall(context), - // ), - // Spacer(), - // ], - // ), - // ), - // ), - // ), + RawMaterialButton( + constraints: const BoxConstraints( + minWidth: 66, + ), + 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.buyDesktop, + width: 24, + height: 24, + ), + const SizedBox( + height: 4, + ), + Text( + "Buy", + style: STextStyles.buttonSmall(context), + ), + const Spacer(), + ], + ), + ), + ), + ), + const SizedBox( + width: 12, + ), ], ), ), diff --git a/lib/pages_desktop_specific/desktop_buy/desktop_buy_view.dart b/lib/pages_desktop_specific/desktop_buy/desktop_buy_view.dart new file mode 100644 index 000000000..45f9cc0be --- /dev/null +++ b/lib/pages_desktop_specific/desktop_buy/desktop_buy_view.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/buy_view/buy_form.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopBuyView extends StatefulWidget { + const DesktopBuyView({Key? key}) : super(key: key); + + static const String routeName = "/desktopBuyView"; + + @override + State createState() => _DesktopBuyViewState(); +} + +class _DesktopBuyViewState extends State { + @override + Widget build(BuildContext context) { + return DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Padding( + padding: const EdgeInsets.only( + left: 24, + ), + child: Text( + "Buy crypto", + style: STextStyles.desktopH3(context), + ), + ), + ), + body: Padding( + padding: const EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + SizedBox( + height: 16, + ), + RoundedWhiteContainer( + padding: EdgeInsets.all(24), + child: BuyForm(), + ), + ], + ), + ), + const SizedBox( + width: 16, + ), + // Expanded( + // child: Row( + // children: const [ + // Expanded( + // child: DesktopTradeHistory(), + // ), + // ], + // ), + // ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/desktop_home_view.dart b/lib/pages_desktop_specific/desktop_home_view.dart index 3ef490512..e9092fe2d 100644 --- a/lib/pages_desktop_specific/desktop_home_view.dart +++ b/lib/pages_desktop_specific/desktop_home_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_buy/desktop_buy_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_menu.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; @@ -56,6 +57,11 @@ class _DesktopHomeViewState extends ConsumerState { onGenerateRoute: RouteGenerator.generateRoute, initialRoute: DesktopExchangeView.routeName, ), + DesktopMenuItemId.buy: const Navigator( + key: Key("desktopBuyHomeKey"), + onGenerateRoute: RouteGenerator.generateRoute, + initialRoute: DesktopBuyView.routeName, + ), DesktopMenuItemId.notifications: const Navigator( key: Key("desktopNotificationsHomeKey"), onGenerateRoute: RouteGenerator.generateRoute, diff --git a/lib/pages_desktop_specific/desktop_menu.dart b/lib/pages_desktop_specific/desktop_menu.dart index 96b4e6638..b705a33be 100644 --- a/lib/pages_desktop_specific/desktop_menu.dart +++ b/lib/pages_desktop_specific/desktop_menu.dart @@ -12,6 +12,7 @@ import 'package:stackwallet/widgets/desktop/living_stack_icon.dart'; enum DesktopMenuItemId { myStack, exchange, + buy, notifications, addressBook, settings, @@ -42,6 +43,8 @@ class _DesktopMenuState extends ConsumerState { double _width = expandedWidth; + // final _buyDataLoadingService = BuyDataLoadingService(); + void updateSelectedMenuItem(DesktopMenuItemId idKey) { widget.onSelectionWillChange?.call(idKey); @@ -73,6 +76,7 @@ class _DesktopMenuState extends ConsumerState { DMIController(), DMIController(), DMIController(), + DMIController(), ]; super.initState(); @@ -157,13 +161,24 @@ class _DesktopMenuState extends ConsumerState { const SizedBox( height: 2, ), + DesktopMenuItem( + duration: duration, + icon: const DesktopBuyIcon(), + label: "Buy crypto", + value: DesktopMenuItemId.buy, + onChanged: updateSelectedMenuItem, + controller: controllers[2], + ), + const SizedBox( + height: 2, + ), DesktopMenuItem( duration: duration, icon: const DesktopNotificationsIcon(), label: "Notifications", value: DesktopMenuItemId.notifications, onChanged: updateSelectedMenuItem, - controller: controllers[2], + controller: controllers[3], ), const SizedBox( height: 2, @@ -174,7 +189,7 @@ class _DesktopMenuState extends ConsumerState { label: "Address Book", value: DesktopMenuItemId.addressBook, onChanged: updateSelectedMenuItem, - controller: controllers[3], + controller: controllers[4], ), const SizedBox( height: 2, @@ -185,7 +200,7 @@ class _DesktopMenuState extends ConsumerState { label: "Settings", value: DesktopMenuItemId.settings, onChanged: updateSelectedMenuItem, - controller: controllers[4], + controller: controllers[5], ), const SizedBox( height: 2, @@ -196,7 +211,7 @@ class _DesktopMenuState extends ConsumerState { label: "Support", value: DesktopMenuItemId.support, onChanged: updateSelectedMenuItem, - controller: controllers[5], + controller: controllers[6], ), const SizedBox( height: 2, @@ -207,7 +222,7 @@ class _DesktopMenuState extends ConsumerState { label: "About", value: DesktopMenuItemId.about, onChanged: updateSelectedMenuItem, - controller: controllers[6], + controller: controllers[7], ), const Spacer(), DesktopMenuItem( @@ -221,7 +236,7 @@ class _DesktopMenuState extends ConsumerState { // exit(0); SystemNavigator.pop(); }, - controller: controllers[7], + controller: controllers[8], ), ], ), diff --git a/lib/pages_desktop_specific/desktop_menu_item.dart b/lib/pages_desktop_specific/desktop_menu_item.dart index 804268797..3e2fa015c 100644 --- a/lib/pages_desktop_specific/desktop_menu_item.dart +++ b/lib/pages_desktop_specific/desktop_menu_item.dart @@ -55,6 +55,26 @@ class DesktopExchangeIcon extends ConsumerWidget { } } +class DesktopBuyIcon extends ConsumerWidget { + const DesktopBuyIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SvgPicture.asset( + Assets.svg.buyDesktop, + width: 20, + height: 20, + color: DesktopMenuItemId.buy == + ref.watch(currentDesktopMenuItemProvider.state).state + ? Theme.of(context).extension()!.accentColorDark + : Theme.of(context) + .extension()! + .accentColorDark + .withOpacity(0.8), + ); + } +} + class DesktopNotificationsIcon extends ConsumerWidget { const DesktopNotificationsIcon({Key? key}) : super(key: key); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 5c1e048a0..c94b61550 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -6,8 +6,6 @@ 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/exchange_view/sub_widgets/exchange_rate_sheet.dart'; -import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_button.dart'; @@ -25,8 +23,6 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; -import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; -import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -44,8 +40,6 @@ import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/hover_text_field.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -import 'package:stackwallet/widgets/stack_dialog.dart'; -import 'package:tuple/tuple.dart'; /// [eventBus] should only be set during testing class DesktopWalletView extends ConsumerStatefulWidget { @@ -70,8 +64,6 @@ class _DesktopWalletViewState extends ConsumerState { late final bool _shouldDisableAutoSyncOnLogOut; - final _cnLoadingService = ExchangeDataLoadingService(); - Future onBackPressed() async { await _logout(); if (mounted) { @@ -96,87 +88,6 @@ class _DesktopWalletViewState extends ConsumerState { ref.read(managerProvider.notifier).isActiveWallet = false; } - void _loadCNData() { - // unawaited future - if (ref.read(prefsChangeNotifierProvider).externalCalls) { - _cnLoadingService.loadAll(ref, - coin: ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .coin); - } else { - Logging.instance.log("User does not want to use external calls", - level: LogLevel.Info); - } - } - - void _onExchangePressed(BuildContext context) async { - final managerProvider = ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(widget.walletId); - unawaited(_cnLoadingService.loadAll(ref)); - - final coin = ref.read(managerProvider).coin; - - if (coin == Coin.epicCash) { - await showDialog( - context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for Epic Cash", - ), - ); - } else if (coin.name.endsWith("TestNet")) { - await showDialog( - context: context, - builder: (_) => const StackOkDialog( - title: "Exchange not available for test net coins", - ), - ); - } else { - ref.read(currentExchangeNameStateProvider.state).state = - ChangeNowExchange.exchangeName; - ref.read(prefsChangeNotifierProvider).exchangeRateType = - ExchangeRateType.estimated; - - ref.read(exchangeFormStateProvider).exchange = ref.read(exchangeProvider); - ref.read(exchangeFormStateProvider).exchangeType = - ExchangeRateType.estimated; - - final currencies = ref - .read(availableChangeNowCurrenciesProvider) - .currencies - .where((element) => - element.ticker.toLowerCase() == coin.ticker.toLowerCase()); - - if (currencies.isNotEmpty) { - ref.read(exchangeFormStateProvider).setCurrencies( - currencies.first, - ref - .read(availableChangeNowCurrenciesProvider) - .currencies - .firstWhere( - (element) => - element.ticker.toLowerCase() != - coin.ticker.toLowerCase(), - ), - ); - } - - if (mounted) { - unawaited( - Navigator.of(context).pushNamed( - WalletInitiatedExchangeView.routeName, - arguments: Tuple3( - widget.walletId, - coin, - _loadCNData, - ), - ), - ); - } - } - } - Future attemptAnonymize() async { final managerProvider = ref .read(walletsChangeNotifierProvider) diff --git a/lib/providers/buy/buy_form_state_provider.dart b/lib/providers/buy/buy_form_state_provider.dart new file mode 100644 index 000000000..2a0dc719c --- /dev/null +++ b/lib/providers/buy/buy_form_state_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/buy/buy_form_state.dart'; + +final buyFormStateProvider = ChangeNotifierProvider( + (ref) => BuyFormState(), +); diff --git a/lib/providers/buy/simplex_initial_load_status.dart b/lib/providers/buy/simplex_initial_load_status.dart new file mode 100644 index 000000000..9571f5b64 --- /dev/null +++ b/lib/providers/buy/simplex_initial_load_status.dart @@ -0,0 +1,11 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +enum SimplexLoadStatus { + waiting, + loading, + success, + failed, +} + +final simplexLoadStatusStateProvider = + StateProvider((ref) => SimplexLoadStatus.waiting); diff --git a/lib/providers/buy/simplex_provider.dart b/lib/providers/buy/simplex_provider.dart new file mode 100644 index 000000000..d15d335a5 --- /dev/null +++ b/lib/providers/buy/simplex_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/buy/simplex/simplex.dart'; + +final simplexProvider = Provider( + (ref) => Simplex(), +); diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 5cef38830..210d36420 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -1,3 +1,6 @@ +export './buy/buy_form_state_provider.dart'; +export './buy/simplex_initial_load_status.dart'; +export './buy/simplex_provider.dart'; export './exchange/available_changenow_currencies_provider.dart'; export './exchange/available_simpleswap_currencies_provider.dart'; export './exchange/changenow_initial_load_status.dart'; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 8ebdd36a2..af88499ba 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -2,6 +2,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; @@ -21,6 +22,7 @@ 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_quote_preview.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'; @@ -87,6 +89,8 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart'; +// import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_buys_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_buy/desktop_buy_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; @@ -1045,6 +1049,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case BuyQuotePreviewView.routeName: + if (args is SimplexQuote) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => BuyQuotePreviewView( + quote: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == Desktop specific routes ============================================ case CreatePasswordView.routeName: if (args is bool) { @@ -1107,6 +1125,12 @@ class RouteGenerator { builder: (_) => const DesktopExchangeView(), settings: RouteSettings(name: settings.name)); + case DesktopBuyView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const DesktopBuyView(), + settings: RouteSettings(name: settings.name)); + case DesktopAllTradesView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/buy/buy.dart b/lib/services/buy/buy.dart new file mode 100644 index 000000000..84e38487c --- /dev/null +++ b/lib/services/buy/buy.dart @@ -0,0 +1,3 @@ +abstract class Buy { + String get name; +} diff --git a/lib/services/buy/buy_data_loading_service.dart b/lib/services/buy/buy_data_loading_service.dart new file mode 100644 index 000000000..93999c221 --- /dev/null +++ b/lib/services/buy/buy_data_loading_service.dart @@ -0,0 +1,62 @@ +// import 'package:flutter_riverpod/flutter_riverpod.dart'; +// import 'package:stackwallet/providers/providers.dart'; +// import 'package:stackwallet/services/buy/simplex/simplex_api.dart'; +// import 'package:stackwallet/utilities/logger.dart'; +// +// class BuyDataLoadingService { +// Future loadAll(WidgetRef ref) async { +// try { +// await Future.wait([ +// _loadSimplexCurrencies(ref), +// ]); +// } catch (e, s) { +// Logging.instance.log("BuyDataLoadingService.loadAll failed: $e\n$s", +// level: LogLevel.Error); +// } +// } +// +// Future _loadSimplexCurrencies(WidgetRef ref) async { +// bool error = false; +// // if (ref.read(simplexLoadStatusStateProvider.state).state == +// // SimplexLoadStatus.loading) { +// // // already in progress so just +// // return; +// // } +// +// ref.read(simplexLoadStatusStateProvider.state).state = +// SimplexLoadStatus.loading; +// +// final response = await SimplexAPI.instance.getSupported(); +// +// if (response.value != null) { +// ref +// .read(supportedSimplexCurrenciesProvider) +// .updateSupportedCryptos(response.value!.item1); +// } else { +// error = true; +// Logging.instance.log( +// "_loadSimplexCurrencies: $response", +// level: LogLevel.Warning, +// ); +// } +// +// if (response.value != null) { +// ref +// .read(supportedSimplexCurrenciesProvider) +// .updateSupportedFiats(response.value!.item2); +// } else { +// error = true; +// Logging.instance.log( +// "_loadSimplexCurrencies: $response", +// level: LogLevel.Warning, +// ); +// } +// +// if (error) { +// // _loadSimplexCurrencies() again? +// } else { +// ref.read(simplexLoadStatusStateProvider.state).state = +// SimplexLoadStatus.success; +// } +// } +// } diff --git a/lib/services/buy/buy_response.dart b/lib/services/buy/buy_response.dart new file mode 100644 index 000000000..3198d0a19 --- /dev/null +++ b/lib/services/buy/buy_response.dart @@ -0,0 +1,24 @@ +enum BuyExceptionType { generic, serializeResponseError } + +class BuyException implements Exception { + String errorMessage; + BuyExceptionType type; + BuyException(this.errorMessage, this.type); + + @override + String toString() { + return errorMessage; + } +} + +class BuyResponse { + final T? value; + final BuyException? exception; + + BuyResponse({this.value, this.exception}); + + @override + String toString() { + return "{error: $exception, value: $value}"; + } +} diff --git a/lib/services/buy/simplex/simplex_api.dart b/lib/services/buy/simplex/simplex_api.dart new file mode 100644 index 000000000..9e01609bf --- /dev/null +++ b/lib/services/buy/simplex/simplex_api.dart @@ -0,0 +1,330 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:decimal/decimal.dart'; +import 'package:http/http.dart' as http; +import 'package:stackwallet/models/buy/response_objects/crypto.dart'; +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/fiat_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +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 scheme = authority == "localhost" ? "http" : "https"; + + final _prefs = Prefs.instance; + + SimplexAPI._(); + static final SimplexAPI _instance = SimplexAPI._(); + static SimplexAPI get instance => _instance; + + /// set this to override using standard http client. Useful for testing + http.Client? client; + + Uri _buildUri(String path, Map? params) { + if (scheme == "http") { + return Uri.http(authority, path, params); + } + return Uri.https(authority, path, params); + } + + Future>> getSupportedCryptos() async { + try { + Map headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + Map data = { + 'ROUTE': 'supported_cryptos', + }; + Uri url = _buildUri('api.php', data); + + var res = await http.post(url, headers: headers); + if (res.statusCode != 200) { + throw Exception( + 'getAvailableCurrencies exception: statusCode= ${res.statusCode}'); + } + final jsonArray = jsonDecode(res.body); // TODO handle if invalid json + + return _parseSupportedCryptos(jsonArray); + } catch (e, s) { + Logging.instance.log("getAvailableCurrencies exception: $e\n$s", + level: LogLevel.Error); + return BuyResponse( + exception: BuyException( + e.toString(), + BuyExceptionType.generic, + ), + ); + } + } + + BuyResponse> _parseSupportedCryptos(dynamic jsonArray) { + try { + List cryptos = []; + List fiats = []; + + 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': "", + })); + } + + return BuyResponse(value: cryptos); + } catch (e, s) { + Logging.instance + .log("_parseSupported exception: $e\n$s", level: LogLevel.Error); + return BuyResponse( + exception: BuyException( + e.toString(), + BuyExceptionType.generic, + ), + ); + } + } + + Future>> getSupportedFiats() async { + try { + Map headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + Map data = { + 'ROUTE': 'supported_fiats', + }; + Uri url = _buildUri('api.php', data); + + var res = await http.post(url, headers: headers); + if (res.statusCode != 200) { + throw Exception( + 'getAvailableCurrencies exception: statusCode= ${res.statusCode}'); + } + final jsonArray = jsonDecode(res.body); // TODO validate json + + return _parseSupportedFiats(jsonArray); + } catch (e, s) { + Logging.instance.log("getAvailableCurrencies exception: $e\n$s", + level: LogLevel.Error); + return BuyResponse( + exception: BuyException( + e.toString(), + BuyExceptionType.generic, + ), + ); + } + } + + BuyResponse> _parseSupportedFiats(dynamic jsonArray) { + try { + List cryptos = []; + List fiats = []; + + for (final fiat in jsonArray as List) { + if (isSimplexFiat("${fiat['ticker_symbol']}")) { + // TODO validate list + fiats.add(Fiat.fromJson({ + 'ticker': "${fiat['ticker_symbol']}", + 'name': fiatFromTickerCaseInsensitive("${fiat['ticker_symbol']}") + .prettyName, + 'minAmount': "${fiat['min_amount']}", + 'maxAmount': "${fiat['max_amount']}", + 'image': "", + })); + } // TODO handle else + } + + return BuyResponse(value: fiats); + } catch (e, s) { + Logging.instance + .log("_parseSupported exception: $e\n$s", level: LogLevel.Error); + return BuyResponse( + exception: BuyException( + e.toString(), + BuyExceptionType.generic, + ), + ); + } + } + + Future> getQuote(SimplexQuote quote) async { + try { + await _prefs.init(); + String? userID = _prefs.userID; + + Map headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + Map data = { + 'ROUTE': 'quote', + 'CRYPTO_TICKER': quote.crypto.ticker.toUpperCase(), + 'FIAT_TICKER': quote.fiat.ticker.toUpperCase(), + 'REQUESTED_TICKER': quote.buyWithFiat + ? quote.fiat.ticker.toUpperCase() + : quote.crypto.ticker.toUpperCase(), + 'REQUESTED_AMOUNT': quote.buyWithFiat + ? "${quote.youPayFiatPrice}" + : "${quote.youReceiveCryptoAmount}", + }; + if (userID != null) { + data['USER_ID'] = userID; + } + Uri url = _buildUri('api.php', data); + + var res = await http.get(url, headers: headers); + if (res.statusCode != 200) { + throw Exception('getQuote exception: statusCode= ${res.statusCode}'); + } + final jsonArray = jsonDecode(res.body); + + jsonArray['quote'] = quote; // Add and pass this on + + return _parseQuote(jsonArray); + } catch (e, s) { + Logging.instance.log("getQuote exception: $e\n$s", level: LogLevel.Error); + return BuyResponse( + exception: BuyException( + e.toString(), + BuyExceptionType.generic, + ), + ); + } + } + + BuyResponse _parseQuote(dynamic jsonArray) { + try { + String cryptoAmount = "${jsonArray['digital_money']['amount']}"; + + SimplexQuote quote = jsonArray['quote'] as SimplexQuote; + final SimplexQuote _quote = SimplexQuote( + crypto: quote.crypto, + fiat: quote.fiat, + youPayFiatPrice: quote.buyWithFiat + ? quote.youPayFiatPrice + : Decimal.parse("${jsonArray['fiat_money']['base_amount']}"), + youReceiveCryptoAmount: + Decimal.parse("${jsonArray['digital_money']['amount']}"), + id: jsonArray['quote_id'] as String, + receivingAddress: quote.receivingAddress, + buyWithFiat: quote.buyWithFiat, + ); + + return BuyResponse(value: _quote); + } catch (e, s) { + Logging.instance + .log("_parseQuote exception: $e\n$s", level: LogLevel.Error); + return BuyResponse( + exception: BuyException( + e.toString(), + BuyExceptionType.generic, + ), + ); + } + } + + Future> newOrder(SimplexQuote quote) async { + // Calling Simplex's API manually: + // curl --request POST \ + // --url https://sandbox.test-simplexcc.com/wallet/merchant/v2/payments/partner/data \ + // --header 'Authorization: ApiKey $apiKey' \ + // --header 'accept: application/json' \ + // --header 'content-type: application/json' \ + // -d '{"account_details": {"app_provider_id": "$publicKey", "app_version_id": "123", "app_end_user_id": "01e7a0b9-8dfc-4988-a28d-84a34e5f0a63", "signup_login": {"timestamp": "1994-11-05T08:15:30-05:00", "ip": "207.66.86.226"}}, "transaction_details": {"payment_details": {"quote_id": "3b58f4b4-ed6f-447c-b96a-ffe97d7b6803", "payment_id": "baaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", "order_id": "789", "original_http_ref_url": "https://stackwallet.com/simplex", "destination_wallet": {"currency": "BTC", "address": "bc1qjvj9ca8gdsv3g58yrzrk6jycvgnjh9uj35rja2"}}}}' + try { + await _prefs.init(); + String? userID = _prefs.userID; + int? signupEpoch = _prefs.signupEpoch; + + Map headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + Map data = { + 'ROUTE': 'order', + 'QUOTE_ID': quote.id, + 'ADDRESS': quote.receivingAddress, + 'CRYPTO_TICKER': quote.crypto.ticker.toUpperCase(), + }; + if (userID != null) { + data['USER_ID'] = userID; + } + if (signupEpoch != null && signupEpoch != 0) { + DateTime date = DateTime.fromMillisecondsSinceEpoch(signupEpoch * 1000); + data['SIGNUP_TIMESTAMP'] = + 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 + + SimplexOrder _order = SimplexOrder( + quote: quote, + paymentId: "${jsonArray['paymentId']}", + orderId: "${jsonArray['orderId']}", + userId: "${jsonArray['userId']}", + ); + + return BuyResponse(value: _order); + } catch (e, s) { + Logging.instance.log("newOrder exception: $e\n$s", level: LogLevel.Error); + return BuyResponse( + exception: BuyException( + e.toString(), + BuyExceptionType.generic, + ), + ); + } + } + + Future> redirect(SimplexOrder order) async { + try { + Map headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + Map data = { + 'ROUTE': 'redirect', + 'PAYMENT_ID': order.paymentId, + }; + Uri url = _buildUri('api.php', data); + + bool status = await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); + + return BuyResponse(value: status); + } catch (e, s) { + Logging.instance.log("newOrder exception: $e\n$s", level: LogLevel.Error); + return BuyResponse( + exception: BuyException( + e.toString(), + BuyExceptionType.generic, + )); + } + } + + bool isSimplexFiat(String ticker) { + try { + fiatFromTickerCaseInsensitive(ticker); + return true; + } on ArgumentError catch (_) { + return false; + } + } + + // See https://github.com/dart-lang/sdk/issues/43391#issuecomment-1229656422 + String timeZoneFormatter(Duration offset) => + "${offset.isNegative ? "-" : "+"}${offset.inHours.abs().toString().padLeft(2, "0")}:${(offset.inMinutes - offset.inHours * 60).abs().toString().padLeft(2, "0")}"; +} diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 84ae19483..311a267aa 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -9,6 +9,7 @@ abstract class Assets { static const lottie = _ANIMATIONS(); static const socials = _SOCIALS(); static const exchange = _EXCHANGE(); + static const buy = _BUY(); } class _SOCIALS { @@ -27,6 +28,25 @@ class _EXCHANGE { String get simpleSwap => "assets/svg/exchange_icons/simpleswap-icon.svg"; } +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 simplexLogo(BuildContext context) { + return (Theme.of(context).extension()!.themeType == + ThemeType.dark || + Theme.of(context).extension()!.themeType == + ThemeType + .oledBlack) // TODO make sure this cover OLED black, too + ? "assets/svg/buy/Simplex-Nuvei-Logo-light.svg" + : "assets/svg/buy/Simplex-Nuvei-Logo.svg"; + } +} + class _SVG { const _SVG(); String? background(BuildContext context) { @@ -169,6 +189,7 @@ class _SVG { String get anonymizeFailed => "assets/svg/tx-icon-anonymize-failed.svg"; String get addressBookDesktop => "assets/svg/address-book-desktop.svg"; String get exchangeDesktop => "assets/svg/exchange-desktop.svg"; + String get buyDesktop => "assets/svg/light/buy-coins-icon.svg"; String get aboutDesktop => "assets/svg/about-desktop.svg"; String get walletDesktop => "assets/svg/wallet-desktop.svg"; String get exitDesktop => "assets/svg/exit-desktop.svg"; @@ -189,6 +210,17 @@ 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"; @@ -236,6 +268,33 @@ 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/constants.dart b/lib/utilities/constants.dart index e78d7a76f..ba128ab35 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -21,6 +21,7 @@ abstract class Constants { } static bool enableExchange = Util.isDesktop || !Platform.isIOS; + static bool enableBuy = true; // true for development, TODO change to "Util.isDesktop || !Platform.isIOS;" as above or even just = enableExchange //TODO: correct for monero? static const int _satsPerCoinMonero = 1000000000000; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index df586ebf5..e50e51b44 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -313,8 +313,6 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.particl; case "tltc": return Coin.litecoinTestNet; - case "part": - return Coin.particl; case "tbtc": return Coin.bitcoinTestNet; case "tbch": diff --git a/lib/utilities/enums/fiat_enum.dart b/lib/utilities/enums/fiat_enum.dart new file mode 100644 index 000000000..2a7303c86 --- /dev/null +++ b/lib/utilities/enums/fiat_enum.dart @@ -0,0 +1,1156 @@ +enum Fiats { + AED, + AFN, + ALL, + AMD, + ANG, + AOA, + ARS, + AUD, + AWG, + AZN, + BAM, + BBD, + BDT, + BGN, + BHD, + BIF, + BMD, + BND, + BOB, + BRL, + BSD, + BTN, + BWP, + BYN, + BZD, + CAD, + CDF, + CHF, + CLP, + CNY, + COP, + CRC, + CUC, + CUP, + CVE, + CZK, + DJF, + DKK, + DOP, + DZD, + EGP, + ERN, + ETB, + EUR, + FJD, + FKP, + GBP, + GEL, + GGP, + GHS, + GIP, + GMD, + GNF, + GTQ, + GYD, + HKD, + HNL, + HRK, + HTG, + HUF, + IDR, + ILS, + IMP, + INR, + IQD, + IRR, + ISK, + JEP, + JMD, + JOD, + JPY, + KES, + KGS, + KHR, + KMF, + KPW, + KRW, + KWD, + KYD, + KZT, + LAK, + LBP, + LKR, + LRD, + LSL, + LYD, + MAD, + MDL, + MGA, + MKD, + MMK, + MNT, + MOP, + MRU, + MUR, + MVR, + MWK, + MXN, + MYR, + MZN, + NAD, + NGN, + NIO, + NOK, + NPR, + NZD, + OMR, + PAB, + PEN, + PGK, + PHP, + PKR, + PLN, + PYG, + QAR, + RON, + RSD, + RUB, + RWF, + SAR, + SBD, + SCR, + SDG, + SEK, + SGD, + SHP, + SLL, + SOS, + SPL, + SRD, + STN, + SVC, + SYP, + SZL, + THB, + TJS, + TMT, + TND, + TOP, + TRY, + TTD, + TVD, + TWD, + TZS, + UAH, + UGX, + USD, + UYU, + UZS, + VEF, + VND, + VUV, + WST, + XAF, + XCD, + XDR, + XOF, + XPF, + YER, + ZAR, + ZMW, + ZWD, +} + +extension FiatExt on Fiats { + String get ticker { + switch (this) { + case Fiats.AED: + return 'AED'; + case Fiats.AFN: + return 'AFN'; + case Fiats.ALL: + return 'ALL'; + case Fiats.AMD: + return 'AMD'; + case Fiats.ANG: + return 'ANG'; + case Fiats.AOA: + return 'AOA'; + case Fiats.ARS: + return 'ARS'; + case Fiats.AUD: + return 'AUD'; + case Fiats.AWG: + return 'AWG'; + case Fiats.AZN: + return 'AZN'; + case Fiats.BAM: + return 'BAM'; + case Fiats.BBD: + return 'BBD'; + case Fiats.BDT: + return 'BDT'; + case Fiats.BGN: + return 'BGN'; + case Fiats.BHD: + return 'BHD'; + case Fiats.BIF: + return 'BIF'; + case Fiats.BMD: + return 'BMD'; + case Fiats.BND: + return 'BND'; + case Fiats.BOB: + return 'BOB'; + case Fiats.BRL: + return 'BRL'; + case Fiats.BSD: + return 'BSD'; + case Fiats.BTN: + return 'BTN'; + case Fiats.BWP: + return 'BWP'; + case Fiats.BYN: + return 'BYN'; + case Fiats.BZD: + return 'BZD'; + case Fiats.CAD: + return 'CAD'; + case Fiats.CDF: + return 'CDF'; + case Fiats.CHF: + return 'CHF'; + case Fiats.CLP: + return 'CLP'; + case Fiats.CNY: + return 'CNY'; + case Fiats.COP: + return 'COP'; + case Fiats.CRC: + return 'CRC'; + case Fiats.CUC: + return 'CUC'; + case Fiats.CUP: + return 'CUP'; + case Fiats.CVE: + return 'CVE'; + case Fiats.CZK: + return 'CZK'; + case Fiats.DJF: + return 'DJF'; + case Fiats.DKK: + return 'DKK'; + case Fiats.DOP: + return 'DOP'; + case Fiats.DZD: + return 'DZD'; + case Fiats.EGP: + return 'EGP'; + case Fiats.ERN: + return 'ERN'; + case Fiats.ETB: + return 'ETB'; + case Fiats.EUR: + return 'EUR'; + case Fiats.FJD: + return 'FJD'; + case Fiats.FKP: + return 'FKP'; + case Fiats.GBP: + return 'GBP'; + case Fiats.GEL: + return 'GEL'; + case Fiats.GGP: + return 'GGP'; + case Fiats.GHS: + return 'GHS'; + case Fiats.GIP: + return 'GIP'; + case Fiats.GMD: + return 'GMD'; + case Fiats.GNF: + return 'GNF'; + case Fiats.GTQ: + return 'GTQ'; + case Fiats.GYD: + return 'GYD'; + case Fiats.HKD: + return 'HKD'; + case Fiats.HNL: + return 'HNL'; + case Fiats.HRK: + return 'HRK'; + case Fiats.HTG: + return 'HTG'; + case Fiats.HUF: + return 'HUF'; + case Fiats.IDR: + return 'IDR'; + case Fiats.ILS: + return 'ILS'; + case Fiats.IMP: + return 'IMP'; + case Fiats.INR: + return 'INR'; + case Fiats.IQD: + return 'IQD'; + case Fiats.IRR: + return 'IRR'; + case Fiats.ISK: + return 'ISK'; + case Fiats.JEP: + return 'JEP'; + case Fiats.JMD: + return 'JMD'; + case Fiats.JOD: + return 'JOD'; + case Fiats.JPY: + return 'JPY'; + case Fiats.KES: + return 'KES'; + case Fiats.KGS: + return 'KGS'; + case Fiats.KHR: + return 'KHR'; + case Fiats.KMF: + return 'KMF'; + case Fiats.KPW: + return 'KPW'; + case Fiats.KRW: + return 'KRW'; + case Fiats.KWD: + return 'KWD'; + case Fiats.KYD: + return 'KYD'; + case Fiats.KZT: + return 'KZT'; + case Fiats.LAK: + return 'LAK'; + case Fiats.LBP: + return 'LBP'; + case Fiats.LKR: + return 'LKR'; + case Fiats.LRD: + return 'LRD'; + case Fiats.LSL: + return 'LSL'; + case Fiats.LYD: + return 'LYD'; + case Fiats.MAD: + return 'MAD'; + case Fiats.MDL: + return 'MDL'; + case Fiats.MGA: + return 'MGA'; + case Fiats.MKD: + return 'MKD'; + case Fiats.MMK: + return 'MMK'; + case Fiats.MNT: + return 'MNT'; + case Fiats.MOP: + return 'MOP'; + case Fiats.MRU: + return 'MRU'; + case Fiats.MUR: + return 'MUR'; + case Fiats.MVR: + return 'MVR'; + case Fiats.MWK: + return 'MWK'; + case Fiats.MXN: + return 'MXN'; + case Fiats.MYR: + return 'MYR'; + case Fiats.MZN: + return 'MZN'; + case Fiats.NAD: + return 'NAD'; + case Fiats.NGN: + return 'NGN'; + case Fiats.NIO: + return 'NIO'; + case Fiats.NOK: + return 'NOK'; + case Fiats.NPR: + return 'NPR'; + case Fiats.NZD: + return 'NZD'; + case Fiats.OMR: + return 'OMR'; + case Fiats.PAB: + return 'PAB'; + case Fiats.PEN: + return 'PEN'; + case Fiats.PGK: + return 'PGK'; + case Fiats.PHP: + return 'PHP'; + case Fiats.PKR: + return 'PKR'; + case Fiats.PLN: + return 'PLN'; + case Fiats.PYG: + return 'PYG'; + case Fiats.QAR: + return 'QAR'; + case Fiats.RON: + return 'RON'; + case Fiats.RSD: + return 'RSD'; + case Fiats.RUB: + return 'RUB'; + case Fiats.RWF: + return 'RWF'; + case Fiats.SAR: + return 'SAR'; + case Fiats.SBD: + return 'SBD'; + case Fiats.SCR: + return 'SCR'; + case Fiats.SDG: + return 'SDG'; + case Fiats.SEK: + return 'SEK'; + case Fiats.SGD: + return 'SGD'; + case Fiats.SHP: + return 'SHP'; + case Fiats.SLL: + return 'SLL'; + case Fiats.SOS: + return 'SOS'; + case Fiats.SPL: + return 'SPL'; + case Fiats.SRD: + return 'SRD'; + case Fiats.STN: + return 'STN'; + case Fiats.SVC: + return 'SVC'; + case Fiats.SYP: + return 'SYP'; + case Fiats.SZL: + return 'SZL'; + case Fiats.THB: + return 'THB'; + case Fiats.TJS: + return 'TJS'; + case Fiats.TMT: + return 'TMT'; + case Fiats.TND: + return 'TND'; + case Fiats.TOP: + return 'TOP'; + case Fiats.TRY: + return 'TRY'; + case Fiats.TTD: + return 'TTD'; + case Fiats.TVD: + return 'TVD'; + case Fiats.TWD: + return 'TWD'; + case Fiats.TZS: + return 'TZS'; + case Fiats.UAH: + return 'UAH'; + case Fiats.UGX: + return 'UGX'; + case Fiats.USD: + return 'USD'; + case Fiats.UYU: + return 'UYU'; + case Fiats.UZS: + return 'UZS'; + case Fiats.VEF: + return 'VEF'; + case Fiats.VND: + return 'VND'; + case Fiats.VUV: + return 'VUV'; + case Fiats.WST: + return 'WST'; + case Fiats.XAF: + return 'XAF'; + case Fiats.XCD: + return 'XCD'; + case Fiats.XDR: + return 'XDR'; + case Fiats.XOF: + return 'XOF'; + case Fiats.XPF: + return 'XPF'; + case Fiats.YER: + return 'YER'; + case Fiats.ZAR: + return 'ZAR'; + case Fiats.ZMW: + return 'ZMW'; + case Fiats.ZWD: + return 'ZWD'; + } + } + + String get prettyName { + switch (this) { + case Fiats.AED: + return 'United Arab Emirates Dirham'; + case Fiats.AFN: + return 'Afghanistan Afghani'; + case Fiats.ALL: + return 'Albania Lek'; + case Fiats.AMD: + return 'Armenia Dram'; + case Fiats.ANG: + return 'Netherlands Antilles Guilder'; + case Fiats.AOA: + return 'Angola Kwanza'; + case Fiats.ARS: + return 'Argentina Peso'; + case Fiats.AUD: + return 'Australia Dollar'; + case Fiats.AWG: + return 'Aruba Guilder'; + case Fiats.AZN: + return 'Azerbaijan Manat'; + case Fiats.BAM: + return 'Bosnia and Herzegovina Convertible Mark'; + case Fiats.BBD: + return 'Barbados Dollar'; + case Fiats.BDT: + return 'Bangladesh Taka'; + case Fiats.BGN: + return 'Bulgaria Lev'; + case Fiats.BHD: + return 'Bahrain Dinar'; + case Fiats.BIF: + return 'Burundi Franc'; + case Fiats.BMD: + return 'Bermuda Dollar'; + case Fiats.BND: + return 'Brunei Darussalam Dollar'; + case Fiats.BOB: + return 'Bolivia Bolíviano'; + case Fiats.BRL: + return 'Brazil Real'; + case Fiats.BSD: + return 'Bahamas Dollar'; + case Fiats.BTN: + return 'Bhutan Ngultrum'; + case Fiats.BWP: + return 'Botswana Pula'; + case Fiats.BYN: + return 'Belarus Ruble'; + case Fiats.BZD: + return 'Belize Dollar'; + case Fiats.CAD: + return 'Canada Dollar'; + case Fiats.CDF: + return 'Congo/Kinshasa Franc'; + case Fiats.CHF: + return 'Switzerland Franc'; + case Fiats.CLP: + return 'Chile Peso'; + case Fiats.CNY: + return 'China Yuan Renminbi'; + case Fiats.COP: + return 'Colombia Peso'; + case Fiats.CRC: + return 'Costa Rica Colon'; + case Fiats.CUC: + return 'Cuba Convertible Peso'; + case Fiats.CUP: + return 'Cuba Peso'; + case Fiats.CVE: + return 'Cape Verde Escudo'; + case Fiats.CZK: + return 'Czech Republic Koruna'; + case Fiats.DJF: + return 'Djibouti Franc'; + case Fiats.DKK: + return 'Denmark Krone'; + case Fiats.DOP: + return 'Dominican Republic Peso'; + case Fiats.DZD: + return 'Algeria Dinar'; + case Fiats.EGP: + return 'Egypt Pound'; + case Fiats.ERN: + return 'Eritrea Nakfa'; + case Fiats.ETB: + return 'Ethiopia Birr'; + case Fiats.EUR: + return 'Euro Member Countries'; + case Fiats.FJD: + return 'Fiji Dollar'; + case Fiats.FKP: + return 'Falkland Islands (Malvinas) Pound'; + case Fiats.GBP: + return 'United Kingdom Pound'; + case Fiats.GEL: + return 'Georgia Lari'; + case Fiats.GGP: + return 'Guernsey Pound'; + case Fiats.GHS: + return 'Ghana Cedi'; + case Fiats.GIP: + return 'Gibraltar Pound'; + case Fiats.GMD: + return 'Gambia Dalasi'; + case Fiats.GNF: + return 'Guinea Franc'; + case Fiats.GTQ: + return 'Guatemala Quetzal'; + case Fiats.GYD: + return 'Guyana Dollar'; + case Fiats.HKD: + return 'Hong Kong Dollar'; + case Fiats.HNL: + return 'Honduras Lempira'; + case Fiats.HRK: + return 'Croatia Kuna'; + case Fiats.HTG: + return 'Haiti Gourde'; + case Fiats.HUF: + return 'Hungary Forint'; + case Fiats.IDR: + return 'Indonesia Rupiah'; + case Fiats.ILS: + return 'Israel Shekel'; + case Fiats.IMP: + return 'Isle of Man Pound'; + case Fiats.INR: + return 'India Rupee'; + case Fiats.IQD: + return 'Iraq Dinar'; + case Fiats.IRR: + return 'Iran Rial'; + case Fiats.ISK: + return 'Iceland Krona'; + case Fiats.JEP: + return 'Jersey Pound'; + case Fiats.JMD: + return 'Jamaica Dollar'; + case Fiats.JOD: + return 'Jordan Dinar'; + case Fiats.JPY: + return 'Japan Yen'; + case Fiats.KES: + return 'Kenya Shilling'; + case Fiats.KGS: + return 'Kyrgyzstan Som'; + case Fiats.KHR: + return 'Cambodia Riel'; + case Fiats.KMF: + return 'Comorian Franc'; + case Fiats.KPW: + return 'Korea (North) Won'; + case Fiats.KRW: + return 'Korea (South) Won'; + case Fiats.KWD: + return 'Kuwait Dinar'; + case Fiats.KYD: + return 'Cayman Islands Dollar'; + case Fiats.KZT: + return 'Kazakhstan Tenge'; + case Fiats.LAK: + return 'Laos Kip'; + case Fiats.LBP: + return 'Lebanon Pound'; + case Fiats.LKR: + return 'Sri Lanka Rupee'; + case Fiats.LRD: + return 'Liberia Dollar'; + case Fiats.LSL: + return 'Lesotho Loti'; + case Fiats.LYD: + return 'Libya Dinar'; + case Fiats.MAD: + return 'Morocco Dirham'; + case Fiats.MDL: + return 'Moldova Leu'; + case Fiats.MGA: + return 'Madagascar Ariary'; + case Fiats.MKD: + return 'Macedonia Denar'; + case Fiats.MMK: + return 'Myanmar (Burma) Kyat'; + case Fiats.MNT: + return 'Mongolia Tughrik'; + case Fiats.MOP: + return 'Macau Pataca'; + case Fiats.MRU: + return 'Mauritania Ouguiya'; + case Fiats.MUR: + return 'Mauritius Rupee'; + case Fiats.MVR: + return 'Maldives (Maldive Islands) Rufiyaa'; + case Fiats.MWK: + return 'Malawi Kwacha'; + case Fiats.MXN: + return 'Mexico Peso'; + case Fiats.MYR: + return 'Malaysia Ringgit'; + case Fiats.MZN: + return 'Mozambique Metical'; + case Fiats.NAD: + return 'Namibia Dollar'; + case Fiats.NGN: + return 'Nigeria Naira'; + case Fiats.NIO: + return 'Nicaragua Cordoba'; + case Fiats.NOK: + return 'Norway Krone'; + case Fiats.NPR: + return 'Nepal Rupee'; + case Fiats.NZD: + return 'New Zealand Dollar'; + case Fiats.OMR: + return 'Oman Rial'; + case Fiats.PAB: + return 'Panama Balboa'; + case Fiats.PEN: + return 'Peru Sol'; + case Fiats.PGK: + return 'Papua New Guinea Kina'; + case Fiats.PHP: + return 'Philippines Peso'; + case Fiats.PKR: + return 'Pakistan Rupee'; + case Fiats.PLN: + return 'Poland Zloty'; + case Fiats.PYG: + return 'Paraguay Guarani'; + case Fiats.QAR: + return 'Qatar Riyal'; + case Fiats.RON: + return 'Romania Leu'; + case Fiats.RSD: + return 'Serbia Dinar'; + case Fiats.RUB: + return 'Russia Ruble'; + case Fiats.RWF: + return 'Rwanda Franc'; + case Fiats.SAR: + return 'Saudi Arabia Riyal'; + case Fiats.SBD: + return 'Solomon Islands Dollar'; + case Fiats.SCR: + return 'Seychelles Rupee'; + case Fiats.SDG: + return 'Sudan Pound'; + case Fiats.SEK: + return 'Sweden Krona'; + case Fiats.SGD: + return 'Singapore Dollar'; + case Fiats.SHP: + return 'Saint Helena Pound'; + case Fiats.SLL: + return 'Sierra Leone Leone'; + case Fiats.SOS: + return 'Somalia Shilling'; + case Fiats.SPL: + return 'Seborga Luigino'; + case Fiats.SRD: + return 'Suriname Dollar'; + case Fiats.STN: + return 'São Tomé and Príncipe Dobra'; + case Fiats.SVC: + return 'El Salvador Colon'; + case Fiats.SYP: + return 'Syria Pound'; + case Fiats.SZL: + return 'eSwatini Lilangeni'; + case Fiats.THB: + return 'Thailand Baht'; + case Fiats.TJS: + return 'Tajikistan Somoni'; + case Fiats.TMT: + return 'Turkmenistan Manat'; + case Fiats.TND: + return 'Tunisia Dinar'; + case Fiats.TOP: + return "Tonga Pa'anga"; + case Fiats.TRY: + return 'Turkey Lira'; + case Fiats.TTD: + return 'Trinidad and Tobago Dollar'; + case Fiats.TVD: + return 'Tuvalu Dollar'; + case Fiats.TWD: + return 'Taiwan New Dollar'; + case Fiats.TZS: + return 'Tanzania Shilling'; + case Fiats.UAH: + return 'Ukraine Hryvnia'; + case Fiats.UGX: + return 'Uganda Shilling'; + case Fiats.USD: + return 'United States Dollar'; + case Fiats.UYU: + return 'Uruguay Peso'; + case Fiats.UZS: + return 'Uzbekistan Som'; + case Fiats.VEF: + return 'Venezuela Bolívar'; + case Fiats.VND: + return 'Viet Nam Dong'; + case Fiats.VUV: + return 'Vanuatu Vatu'; + case Fiats.WST: + return 'Samoa Tala'; + case Fiats.XAF: + return 'Communauté Financière Africaine (BEAC) CFA Franc BEAC'; + case Fiats.XCD: + return 'East Caribbean Dollar'; + case Fiats.XDR: + return 'International Monetary Fund (IMF) Special Drawing Rights'; + case Fiats.XOF: + return 'Communauté Financière Africaine (BCEAO) Franc'; + case Fiats.XPF: + return 'Comptoirs Français du Pacifique (CFP) Franc'; + case Fiats.YER: + return 'Yemen Rial'; + case Fiats.ZAR: + return 'South Africa Rand'; + case Fiats.ZMW: + return 'Zambia Kwacha'; + case Fiats.ZWD: + return 'Zimbabwe Dollar'; + } + } +} + +Fiats fiatFromTickerCaseInsensitive(String ticker) { + switch (ticker.toLowerCase()) { + case "aed": + return Fiats.AED; + case "afn": + return Fiats.AFN; + case "all": + return Fiats.ALL; + case "amd": + return Fiats.AMD; + case "ang": + return Fiats.ANG; + case "aoa": + return Fiats.AOA; + case "ars": + return Fiats.ARS; + case "aud": + return Fiats.AUD; + case "awg": + return Fiats.AWG; + case "azn": + return Fiats.AZN; + case "bam": + return Fiats.BAM; + case "bbd": + return Fiats.BBD; + case "bdt": + return Fiats.BDT; + case "bgn": + return Fiats.BGN; + case "bhd": + return Fiats.BHD; + case "bif": + return Fiats.BIF; + case "bmd": + return Fiats.BMD; + case "bnd": + return Fiats.BND; + case "bob": + return Fiats.BOB; + case "brl": + return Fiats.BRL; + case "bsd": + return Fiats.BSD; + case "btn": + return Fiats.BTN; + case "bwp": + return Fiats.BWP; + case "byn": + return Fiats.BYN; + case "bzd": + return Fiats.BZD; + case "cad": + return Fiats.CAD; + case "cdf": + return Fiats.CDF; + case "chf": + return Fiats.CHF; + case "clp": + return Fiats.CLP; + case "cny": + return Fiats.CNY; + case "cop": + return Fiats.COP; + case "crc": + return Fiats.CRC; + case "cuc": + return Fiats.CUC; + case "cup": + return Fiats.CUP; + case "cve": + return Fiats.CVE; + case "czk": + return Fiats.CZK; + case "djf": + return Fiats.DJF; + case "dkk": + return Fiats.DKK; + case "dop": + return Fiats.DOP; + case "dzd": + return Fiats.DZD; + case "egp": + return Fiats.EGP; + case "ern": + return Fiats.ERN; + case "etb": + return Fiats.ETB; + case "eur": + return Fiats.EUR; + case "fjd": + return Fiats.FJD; + case "fkp": + return Fiats.FKP; + case "gbp": + return Fiats.GBP; + case "gel": + return Fiats.GEL; + case "ggp": + return Fiats.GGP; + case "ghs": + return Fiats.GHS; + case "gip": + return Fiats.GIP; + case "gmd": + return Fiats.GMD; + case "gnf": + return Fiats.GNF; + case "gtq": + return Fiats.GTQ; + case "gyd": + return Fiats.GYD; + case "hkd": + return Fiats.HKD; + case "hnl": + return Fiats.HNL; + case "hrk": + return Fiats.HRK; + case "htg": + return Fiats.HTG; + case "huf": + return Fiats.HUF; + case "idr": + return Fiats.IDR; + case "ils": + return Fiats.ILS; + case "imp": + return Fiats.IMP; + case "inr": + return Fiats.INR; + case "iqd": + return Fiats.IQD; + case "irr": + return Fiats.IRR; + case "isk": + return Fiats.ISK; + case "jep": + return Fiats.JEP; + case "jmd": + return Fiats.JMD; + case "jod": + return Fiats.JOD; + case "jpy": + return Fiats.JPY; + case "kes": + return Fiats.KES; + case "kgs": + return Fiats.KGS; + case "khr": + return Fiats.KHR; + case "kmf": + return Fiats.KMF; + case "kpw": + return Fiats.KPW; + case "krw": + return Fiats.KRW; + case "kwd": + return Fiats.KWD; + case "kyd": + return Fiats.KYD; + case "kzt": + return Fiats.KZT; + case "lak": + return Fiats.LAK; + case "lbp": + return Fiats.LBP; + case "lkr": + return Fiats.LKR; + case "lrd": + return Fiats.LRD; + case "lsl": + return Fiats.LSL; + case "lyd": + return Fiats.LYD; + case "mad": + return Fiats.MAD; + case "mdl": + return Fiats.MDL; + case "mga": + return Fiats.MGA; + case "mkd": + return Fiats.MKD; + case "mmk": + return Fiats.MMK; + case "mnt": + return Fiats.MNT; + case "mop": + return Fiats.MOP; + case "mru": + return Fiats.MRU; + case "mur": + return Fiats.MUR; + case "mvr": + return Fiats.MVR; + case "mwk": + return Fiats.MWK; + case "mxn": + return Fiats.MXN; + case "myr": + return Fiats.MYR; + case "mzn": + return Fiats.MZN; + case "nad": + return Fiats.NAD; + case "ngn": + return Fiats.NGN; + case "nio": + return Fiats.NIO; + case "nok": + return Fiats.NOK; + case "npr": + return Fiats.NPR; + case "nzd": + return Fiats.NZD; + case "omr": + return Fiats.OMR; + case "pab": + return Fiats.PAB; + case "pen": + return Fiats.PEN; + case "pgk": + return Fiats.PGK; + case "php": + return Fiats.PHP; + case "pkr": + return Fiats.PKR; + case "pln": + return Fiats.PLN; + case "pyg": + return Fiats.PYG; + case "qar": + return Fiats.QAR; + case "ron": + return Fiats.RON; + case "rsd": + return Fiats.RSD; + case "rub": + return Fiats.RUB; + case "rwf": + return Fiats.RWF; + case "sar": + return Fiats.SAR; + case "sbd": + return Fiats.SBD; + case "scr": + return Fiats.SCR; + case "sdg": + return Fiats.SDG; + case "sek": + return Fiats.SEK; + case "sgd": + return Fiats.SGD; + case "shp": + return Fiats.SHP; + case "sll": + return Fiats.SLL; + case "sos": + return Fiats.SOS; + case "spl": + return Fiats.SPL; + case "srd": + return Fiats.SRD; + case "stn": + return Fiats.STN; + case "svc": + return Fiats.SVC; + case "syp": + return Fiats.SYP; + case "szl": + return Fiats.SZL; + case "thb": + return Fiats.THB; + case "tjs": + return Fiats.TJS; + case "tmt": + return Fiats.TMT; + case "tnd": + return Fiats.TND; + case "top": + return Fiats.TOP; + case "try": + return Fiats.TRY; + case "ttd": + return Fiats.TTD; + case "tvd": + return Fiats.TVD; + case "twd": + return Fiats.TWD; + case "tzs": + return Fiats.TZS; + case "uah": + return Fiats.UAH; + case "ugx": + return Fiats.UGX; + case "usd": + return Fiats.USD; + case "uyu": + return Fiats.UYU; + case "uzs": + return Fiats.UZS; + case "vef": + return Fiats.VEF; + case "vnd": + return Fiats.VND; + case "vuv": + return Fiats.VUV; + case "wst": + return Fiats.WST; + case "xaf": + return Fiats.XAF; + case "xcd": + return Fiats.XCD; + case "xdr": + return Fiats.XDR; + case "xof": + return Fiats.XOF; + case "xpf": + return Fiats.XPF; + case "yer": + return Fiats.YER; + case "zar": + return Fiats.ZAR; + case "zmw": + return Fiats.ZMW; + case "zwd": + return Fiats.ZWD; + default: + throw ArgumentError.value( + ticker, "name", "No Fiat enum value with that ticker"); + } +} diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 6b4b9821a..14f95d039 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -5,6 +5,7 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/languages_enum.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; +import 'package:uuid/uuid.dart'; class Prefs extends ChangeNotifier { Prefs._(); @@ -38,6 +39,8 @@ class Prefs extends ChangeNotifier { _startupWalletId = await _getStartupWalletId(); _externalCalls = await _getHasExternalCalls(); _familiarity = await _getHasFamiliarity(); + _userId = await _getUserId(); + _signupEpoch = await _getSignupEpoch(); _initialized = true; } @@ -602,4 +605,45 @@ class Prefs extends ChangeNotifier { } return true; } + + String? _userId; + String? get userID => _userId; + + Future _getUserId() async { + String? userID = await DB.instance + .get(boxName: DB.boxNamePrefs, key: "userID") as String?; + if (userID == null) { + userID = const Uuid().v4(); + await saveUserID(userID); + } + return userID; + } + + Future saveUserID(String userId) async { + _userId = userId; + await DB.instance + .put(boxName: DB.boxNamePrefs, key: "userID", value: _userId); + // notifyListeners(); + } + + int? _signupEpoch; + int? get signupEpoch => _signupEpoch; + + Future _getSignupEpoch() async { + int? signupEpoch = await DB.instance + .get(boxName: DB.boxNamePrefs, key: "signupEpoch") as int?; + if (signupEpoch == null) { + signupEpoch = DateTime.now().millisecondsSinceEpoch ~/ + Duration.millisecondsPerSecond; + await saveSignupEpoch(signupEpoch); + } + return signupEpoch; + } + + Future saveSignupEpoch(int signupEpoch) async { + _signupEpoch = signupEpoch; + await DB.instance.put( + boxName: DB.boxNamePrefs, key: "signupEpoch", value: _signupEpoch); + // notifyListeners(); + } } diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 2eea34806..dfd62a6a0 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -220,9 +220,16 @@ abstract class StackColorTheme { Color get textConfirmTotalAmount; Color get textSelectedWordTableItem; +// rate type toggle + Color get rateTypeToggleColorOn; + Color get rateTypeToggleColorOff; + Color get rateTypeToggleDesktopColorOn; + Color get rateTypeToggleDesktopColorOff; + BoxShadow get standardBoxShadow; BoxShadow? get homeViewButtonBarBoxShadow; } +// 0xFFFFD8CE class CoinThemeColor { const CoinThemeColor(); diff --git a/lib/utilities/theme/dark_colors.dart b/lib/utilities/theme/dark_colors.dart index 298d8c86d..802717f7f 100644 --- a/lib/utilities/theme/dark_colors.dart +++ b/lib/utilities/theme/dark_colors.dart @@ -319,6 +319,16 @@ class DarkColors extends StackColorTheme { @override Color get textSelectedWordTableItem => const Color(0xFF00297A); + //rate type toggle + @override + Color get rateTypeToggleColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleColorOff => popupBG; + @override + Color get rateTypeToggleDesktopColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleDesktopColorOff => buttonBackSecondary; + @override BoxShadow get standardBoxShadow => BoxShadow( color: shadow, diff --git a/lib/utilities/theme/fruit_sorbet_colors.dart b/lib/utilities/theme/fruit_sorbet_colors.dart index 12ae33e7b..6f0a81e82 100644 --- a/lib/utilities/theme/fruit_sorbet_colors.dart +++ b/lib/utilities/theme/fruit_sorbet_colors.dart @@ -319,6 +319,16 @@ class FruitSorbetColors extends StackColorTheme { @override Color get textSelectedWordTableItem => const Color(0xFF232323); + //rate type toggle + @override + Color get rateTypeToggleColorOn => const Color(0xFFFFD8CE); + @override + Color get rateTypeToggleColorOff => popupBG; + @override + Color get rateTypeToggleDesktopColorOn => const Color(0xFFFFD8CE); + @override + Color get rateTypeToggleDesktopColorOff => buttonBackSecondary; + @override BoxShadow get standardBoxShadow => BoxShadow( color: shadow, diff --git a/lib/utilities/theme/light_colors.dart b/lib/utilities/theme/light_colors.dart index fc825747c..b4d66d7f3 100644 --- a/lib/utilities/theme/light_colors.dart +++ b/lib/utilities/theme/light_colors.dart @@ -319,6 +319,16 @@ class LightColors extends StackColorTheme { @override Color get textSelectedWordTableItem => const Color(0xFF232323); + //rate type toggle + @override + Color get rateTypeToggleColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleColorOff => popupBG; + @override + Color get rateTypeToggleDesktopColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleDesktopColorOff => buttonBackSecondary; + @override BoxShadow get standardBoxShadow => BoxShadow( color: shadow, diff --git a/lib/utilities/theme/ocean_breeze_colors.dart b/lib/utilities/theme/ocean_breeze_colors.dart index e907fd2cf..1ba64843d 100644 --- a/lib/utilities/theme/ocean_breeze_colors.dart +++ b/lib/utilities/theme/ocean_breeze_colors.dart @@ -326,6 +326,16 @@ class OceanBreezeColors extends StackColorTheme { @override Color get textSelectedWordTableItem => const Color(0xFF232323); + //rate type toggle + @override + Color get rateTypeToggleColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleColorOff => popupBG; + @override + Color get rateTypeToggleDesktopColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleDesktopColorOff => buttonBackSecondary; + @override BoxShadow get standardBoxShadow => BoxShadow( color: shadow, diff --git a/lib/utilities/theme/oled_black_colors.dart b/lib/utilities/theme/oled_black_colors.dart index 1d9f2c61f..84e2a7f96 100644 --- a/lib/utilities/theme/oled_black_colors.dart +++ b/lib/utilities/theme/oled_black_colors.dart @@ -16,7 +16,7 @@ class OledBlackColors extends StackColorTheme { Color get overlay => const Color(0xFF121212); @override - Color get accentColorBlue => const Color(0xFF77A7F9); + Color get accentColorBlue => const Color(0xFFF26822); @override Color get accentColorGreen => const Color(0xFF4CC0A0); @override @@ -61,17 +61,17 @@ class OledBlackColors extends StackColorTheme { // button background @override - Color get buttonBackPrimary => const Color(0xFF6F9CE9); + Color get buttonBackPrimary => const Color(0xFFF26822); @override Color get buttonBackSecondary => const Color(0xFF1F1F1F); @override - Color get buttonBackPrimaryDisabled => const Color(0xFF212F46); + Color get buttonBackPrimaryDisabled => const Color(0xFF491F0A); @override - Color get buttonBackSecondaryDisabled => const Color(0xFF3D3D3D); + Color get buttonBackSecondaryDisabled => const Color(0xFF0F0F0F); @override - Color get buttonBackBorder => const Color(0xFF6F9CE9); + Color get buttonBackBorder => const Color(0xFFF26822); @override - Color get buttonBackBorderDisabled => const Color(0xFF212F46); + Color get buttonBackBorderDisabled => const Color(0xFF491F0A); @override Color get buttonBackBorderSecondary => buttonBackSecondary; @override @@ -80,7 +80,7 @@ class OledBlackColors extends StackColorTheme { @override Color get numberBackDefault => const Color(0xFF242424); @override - Color get numpadBackDefault => const Color(0xFF6F9CE9); + Color get numpadBackDefault => const Color(0xFFF26822); @override Color get bottomNavBack => const Color(0xFF202122); @@ -92,15 +92,15 @@ class OledBlackColors extends StackColorTheme { @override Color get buttonTextPrimaryDisabled => const Color(0xFF000000); @override - Color get buttonTextSecondaryDisabled => const Color(0xFF090909); + Color get buttonTextSecondaryDisabled => const Color(0xFF6F6F6F); @override - Color get buttonTextBorder => const Color(0xFF6F9CE9); + Color get buttonTextBorder => const Color(0xFFF26822); @override Color get buttonTextDisabled => const Color(0xFF000000); @override - Color get buttonTextBorderless => const Color(0xFF6F9CE9); + Color get buttonTextBorderless => const Color(0xFFF26822); @override - Color get buttonTextBorderlessDisabled => const Color(0xFF212F46); + Color get buttonTextBorderlessDisabled => const Color(0xFF491F0A); @override Color get numberTextDefault => const Color(0xFFD3D3D3); @override @@ -110,27 +110,27 @@ class OledBlackColors extends StackColorTheme { // switch @override - Color get switchBGOn => const Color(0xFF77A7F9); + Color get switchBGOn => const Color(0xFFF26822); @override - Color get switchBGOff => const Color(0xFF445C85); + Color get switchBGOff => const Color(0xFF403F3F); @override Color get switchBGDisabled => const Color(0xFF333538); @override - Color get switchCircleOn => const Color(0xFFC9DDF5); + Color get switchCircleOn => const Color(0xFFFFE8DC); @override - Color get switchCircleOff => const Color(0xFF94AAC9); + Color get switchCircleOff => const Color(0xFFFAF6F3); @override Color get switchCircleDisabled => const Color(0xFF848484); // step indicator background @override - Color get stepIndicatorBGCheck => const Color(0xFF77A7F9); + Color get stepIndicatorBGCheck => const Color(0xFFF26822); @override - Color get stepIndicatorBGNumber => const Color(0xFF77A7F9); + Color get stepIndicatorBGNumber => const Color(0xFFF26822); @override Color get stepIndicatorBGInactive => const Color(0xFF3B3F46); @override - Color get stepIndicatorBGLines => const Color(0xFF6393E5); + Color get stepIndicatorBGLines => const Color(0xFFF26822); @override Color get stepIndicatorBGLinesInactive => const Color(0xFF63676E); @override @@ -138,15 +138,15 @@ class OledBlackColors extends StackColorTheme { @override Color get stepIndicatorIconNumber => const Color(0xFF000000); @override - Color get stepIndicatorIconInactive => const Color(0xFFA5A5A5); + Color get stepIndicatorIconInactive => const Color(0xFFAFAFAF); // checkbox @override - Color get checkboxBGChecked => const Color(0xFF77A7F9); + Color get checkboxBGChecked => const Color(0xFFF26822); @override - Color get checkboxBorderEmpty => const Color(0xFF353536); + Color get checkboxBorderEmpty => const Color(0xFF66696A); @override - Color get checkboxBGDisabled => const Color(0xFF5D759B); + Color get checkboxBGDisabled => const Color(0xFF783818); @override Color get checkboxIconChecked => const Color(0xFF000000); @override @@ -251,17 +251,17 @@ class OledBlackColors extends StackColorTheme { // radio buttons @override - Color get radioButtonIconBorder => const Color(0xFF77A7F9); + Color get radioButtonIconBorder => const Color(0xFFF26822); @override Color get radioButtonIconBorderDisabled => const Color(0xFF7D7D7D); @override - Color get radioButtonBorderEnabled => const Color(0xFF77A7F9); + Color get radioButtonBorderEnabled => const Color(0xFFF26822); @override Color get radioButtonBorderDisabled => const Color(0xFF7D7D7D); @override - Color get radioButtonIconCircle => const Color(0xFF77A7F9); + Color get radioButtonIconCircle => const Color(0xFFF26822); @override - Color get radioButtonIconEnabled => const Color(0xFF77A7F9); + Color get radioButtonIconEnabled => const Color(0xFFF26822); @override Color get radioButtonTextEnabled => const Color(0xFFA8AAB2); @override @@ -279,7 +279,7 @@ class OledBlackColors extends StackColorTheme { @override Color get infoItemText => const Color(0xFFDEDEDE); @override - Color get infoItemIcons => const Color(0xFF77A7F9); + Color get infoItemIcons => const Color(0xFF5C94F4); // popup @override @@ -321,6 +321,16 @@ class OledBlackColors extends StackColorTheme { @override Color get textSelectedWordTableItem => const Color(0xFF143D8E); + //rate type toggle + @override + Color get rateTypeToggleColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleColorOff => popupBG; + @override + Color get rateTypeToggleDesktopColorOn => textFieldDefaultBG; + @override + Color get rateTypeToggleDesktopColorOff => buttonBackSecondary; + @override BoxShadow get standardBoxShadow => BoxShadow( color: shadow, diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 563dd621c..e9280fee5 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -178,6 +178,12 @@ class StackColors extends ThemeExtension { final Color textConfirmTotalAmount; final Color textSelectedWordTableItem; +// rate type toggle + final Color rateTypeToggleColorOn; + final Color rateTypeToggleColorOff; + final Color rateTypeToggleDesktopColorOn; + final Color rateTypeToggleDesktopColorOff; + final BoxShadow standardBoxShadow; final BoxShadow? homeViewButtonBarBoxShadow; @@ -319,6 +325,10 @@ class StackColors extends ThemeExtension { required this.myStackContactIconBG, required this.textConfirmTotalAmount, required this.textSelectedWordTableItem, + required this.rateTypeToggleColorOn, + required this.rateTypeToggleColorOff, + required this.rateTypeToggleDesktopColorOn, + required this.rateTypeToggleDesktopColorOff, required this.standardBoxShadow, required this.homeViewButtonBarBoxShadow, }); @@ -465,6 +475,10 @@ class StackColors extends ThemeExtension { myStackContactIconBG: colorTheme.myStackContactIconBG, textConfirmTotalAmount: colorTheme.textConfirmTotalAmount, textSelectedWordTableItem: colorTheme.textSelectedWordTableItem, + rateTypeToggleColorOn: colorTheme.rateTypeToggleColorOn, + rateTypeToggleColorOff: colorTheme.rateTypeToggleColorOff, + rateTypeToggleDesktopColorOn: colorTheme.rateTypeToggleDesktopColorOn, + rateTypeToggleDesktopColorOff: colorTheme.rateTypeToggleDesktopColorOff, homeViewButtonBarBoxShadow: colorTheme.homeViewButtonBarBoxShadow, standardBoxShadow: colorTheme.standardBoxShadow, ); @@ -609,6 +623,10 @@ class StackColors extends ThemeExtension { Color? myStackContactIconBG, Color? textConfirmTotalAmount, Color? textSelectedWordTableItem, + Color? rateTypeToggleColorOn, + Color? rateTypeToggleColorOff, + Color? rateTypeToggleDesktopColorOn, + Color? rateTypeToggleDesktopColorOff, BoxShadow? homeViewButtonBarBoxShadow, BoxShadow? standardBoxShadow, }) { @@ -790,6 +808,14 @@ class StackColors extends ThemeExtension { textConfirmTotalAmount ?? this.textConfirmTotalAmount, textSelectedWordTableItem: textSelectedWordTableItem ?? this.textSelectedWordTableItem, + rateTypeToggleColorOn: + rateTypeToggleColorOn ?? this.rateTypeToggleColorOn, + rateTypeToggleColorOff: + rateTypeToggleColorOff ?? this.rateTypeToggleColorOff, + rateTypeToggleDesktopColorOn: + rateTypeToggleDesktopColorOn ?? this.rateTypeToggleDesktopColorOn, + rateTypeToggleDesktopColorOff: + rateTypeToggleDesktopColorOff ?? this.rateTypeToggleDesktopColorOff, homeViewButtonBarBoxShadow: homeViewButtonBarBoxShadow ?? this.homeViewButtonBarBoxShadow, standardBoxShadow: standardBoxShadow ?? this.standardBoxShadow, @@ -1483,6 +1509,26 @@ class StackColors extends ThemeExtension { other.textSelectedWordTableItem, t, )!, + rateTypeToggleColorOn: Color.lerp( + rateTypeToggleColorOn, + other.rateTypeToggleColorOn, + t, + )!, + rateTypeToggleColorOff: Color.lerp( + rateTypeToggleColorOff, + other.rateTypeToggleColorOff, + t, + )!, + rateTypeToggleDesktopColorOn: Color.lerp( + rateTypeToggleDesktopColorOn, + other.rateTypeToggleDesktopColorOn, + t, + )!, + rateTypeToggleDesktopColorOff: Color.lerp( + rateTypeToggleDesktopColorOff, + other.rateTypeToggleDesktopColorOff, + t, + )!, ); } diff --git a/pubspec.yaml b/pubspec.yaml index 32ab70261..be53f0330 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.5.30+105 +version: 1.5.35+110 environment: sdk: ">=2.17.0 <3.0.0" @@ -320,6 +320,17 @@ 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 @@ -425,6 +436,10 @@ flutter: - assets/svg/fruitSorbet/buy-coins-icon.svg - assets/svg/fruitSorbet/bg.svg + # buy + - assets/svg/buy/Simplex-Nuvei-Logo.svg + - assets/svg/buy/Simplex-Nuvei-Logo-light.svg + # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see diff --git a/test/models/type_adapter_tests/lelantus_coin_adapter_test.dart b/test/models/type_adapter_tests/lelantus_coin_adapter_test.dart index 41f5d1232..44c2a1a70 100644 --- a/test/models/type_adapter_tests/lelantus_coin_adapter_test.dart +++ b/test/models/type_adapter_tests/lelantus_coin_adapter_test.dart @@ -84,12 +84,12 @@ void main() { ]); }); - test("get hashcode", () { - final adapter = LelantusCoinAdapter(); - - final result = adapter.hashCode; - expect(result, 9); - }); + // test("get hashcode", () { + // final adapter = LelantusCoinAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 9); + // }); group("compare operator", () { test("is equal one", () { diff --git a/test/models/type_adapter_tests/transactions_model_adapter_test.dart b/test/models/type_adapter_tests/transactions_model_adapter_test.dart index d19446a66..a7c9d0441 100644 --- a/test/models/type_adapter_tests/transactions_model_adapter_test.dart +++ b/test/models/type_adapter_tests/transactions_model_adapter_test.dart @@ -52,12 +52,12 @@ void main() { ]); }); - test("TransactionDataAdapter.hashcode", () { - final adapter = TransactionDataAdapter(); - - final result = adapter.hashCode; - expect(result, 1); - }); + // test("TransactionDataAdapter.hashcode", () { + // final adapter = TransactionDataAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 1); + // }); group("TransactionDataAdapter compare operator", () { test("TransactionDataAdapter is equal one", () { @@ -147,12 +147,12 @@ void main() { ]); }); - test("TransactionChunkAdapter.hashcode", () { - final adapter = TransactionChunkAdapter(); - - final result = adapter.hashCode; - expect(result, 2); - }); + // test("TransactionChunkAdapter.hashcode", () { + // final adapter = TransactionChunkAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 2); + // }); group("TransactionChunkAdapter compare operator", () { test("TransactionChunkAdapter is equal one", () { @@ -377,12 +377,12 @@ void main() { ]); }); - test("TransactionAdapter.hashcode", () { - final adapter = TransactionAdapter(); - - final result = adapter.hashCode; - expect(result, 3); - }); + // test("TransactionAdapter.hashcode", () { + // final adapter = TransactionAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 3); + // }); group("TransactionAdapter compare operator", () { test("TransactionAdapter is equal one", () { @@ -401,7 +401,7 @@ void main() { expect(result, true); }); - test("TransactionAdapteris not equal one", () { + test("TransactionAdapter is not equal one", () { final a = TransactionAdapter(); final b = TransactionDataAdapter(); @@ -517,12 +517,12 @@ void main() { ]); }); - test("InputAdapter.hashcode", () { - final adapter = InputAdapter(); - - final result = adapter.hashCode; - expect(result, 4); - }); + // test("InputAdapter.hashcode", () { + // final adapter = InputAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 4); + // }); group("InputAdapter compare operator", () { test("InputAdapter is equal one", () { @@ -633,12 +633,12 @@ void main() { ]); }); - test("OutputAdapter.hashcode", () { - final adapter = OutputAdapter(); - - final result = adapter.hashCode; - expect(result, 5); - }); + // test("OutputAdapter.hashcode", () { + // final adapter = OutputAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 5); + // }); group("OutputAdapter compare operator", () { test("OutputAdapter is equal one", () { diff --git a/test/models/type_adapter_tests/utxo_model_adapter_test.dart b/test/models/type_adapter_tests/utxo_model_adapter_test.dart index 498044976..9dfd5b0ba 100644 --- a/test/models/type_adapter_tests/utxo_model_adapter_test.dart +++ b/test/models/type_adapter_tests/utxo_model_adapter_test.dart @@ -87,12 +87,12 @@ void main() { ]); }); - test("UtxoDataAdapter.hashcode", () { - final adapter = UtxoDataAdapter(); - - final result = adapter.hashCode; - expect(result, 6); - }); + // test("UtxoDataAdapter.hashcode", () { + // final adapter = UtxoDataAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 6); + // }); group("UtxoDataAdapter compare operator", () { test("UtxoDataAdapter is equal one", () { @@ -238,12 +238,12 @@ void main() { ]); }); - test("UtxoObjectAdapter.hashcode", () { - final adapter = UtxoObjectAdapter(); - - final result = adapter.hashCode; - expect(result, 7); - }); + // test("UtxoObjectAdapter.hashcode", () { + // final adapter = UtxoObjectAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 7); + // }); group("UtxoObjectAdapter compare operator", () { test("UtxoObjectAdapter is equal one", () { @@ -359,12 +359,12 @@ void main() { ]); }); - test("StatusAdapter.hashcode", () { - final adapter = StatusAdapter(); - - final result = adapter.hashCode; - expect(result, 8); - }); + // test("StatusAdapter.hashcode", () { + // final adapter = StatusAdapter(); + // + // final result = adapter.hashCode; + // expect(result, 8); + // }); group("StatusAdapter compare operator", () { test("StatusAdapter is equal one", () {