diff --git a/lib/models/buy/response_objects/fiat.dart b/lib/models/buy/response_objects/fiat.dart new file mode 100644 index 000000000..99433a6be --- /dev/null +++ b/lib/models/buy/response_objects/fiat.dart @@ -0,0 +1,123 @@ +class Fiat { + /// Fiat ticker + final String ticker; + + /// Fiat name + final String name; + + /// Fiat network + final String network; + + /// Fiat logo url + final String image; + + /// Indicates if a currency has an Extra ID + final bool hasExternalId; + + /// external id if it exists + final String? externalId; + + /// Indicates if a currency is a fiat currency (EUR, USD) + final bool isFiat; + + /// Indicates if a currency is popular + final bool featured; + + /// Indicates if a currency is stable + final bool isStable; + + /// Indicates if a currency is available on a fixed-rate flow + final bool supportsFixedRate; + + /// (Optional - based on api call) Indicates whether the pair is + /// currently supported by change now + final bool? isAvailable; + + Fiat({ + required this.ticker, + required this.name, + required this.network, + required this.image, + required this.hasExternalId, + this.externalId, + required this.isFiat, + required this.featured, + required this.isStable, + required this.supportsFixedRate, + this.isAvailable, + }); + + factory Fiat.fromJson(Map json) { + try { + return Fiat( + ticker: json["ticker"] as String, + name: json["name"] as String, + network: json["network"] as String? ?? "", + image: json["image"] as String, + hasExternalId: json["hasExternalId"] as bool, + externalId: json["externalId"] as String?, + isFiat: json["isFiat"] as bool, + featured: json["featured"] as bool, + isStable: json["isStable"] as bool, + supportsFixedRate: json["supportsFixedRate"] as bool, + isAvailable: json["isAvailable"] as bool?, + ); + } catch (e) { + rethrow; + } + } + + Map toJson() { + final map = { + "ticker": ticker, + "name": name, + "network": network, + "image": image, + "hasExternalId": hasExternalId, + "externalId": externalId, + "isFiat": isFiat, + "featured": featured, + "isStable": isStable, + "supportsFixedRate": supportsFixedRate, + }; + + if (isAvailable != null) { + map["isAvailable"] = isAvailable!; + } + + return map; + } + + Fiat copyWith({ + String? ticker, + String? name, + String? network, + String? image, + bool? hasExternalId, + String? externalId, + bool? isFiat, + bool? featured, + bool? isStable, + bool? supportsFixedRate, + bool? isAvailable, + }) { + return Fiat( + ticker: ticker ?? this.ticker, + name: name ?? this.name, + network: network ?? this.network, + image: image ?? this.image, + hasExternalId: hasExternalId ?? this.hasExternalId, + externalId: externalId ?? this.externalId, + isFiat: isFiat ?? this.isFiat, + featured: featured ?? this.featured, + isStable: isStable ?? this.isStable, + supportsFixedRate: supportsFixedRate ?? this.supportsFixedRate, + isAvailable: isAvailable ?? this.isAvailable, + ); + } + + @override + String toString() { + return "Fiat: ${toJson()}"; + } +} diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index 0bf13c36d..7e915db76 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -2,9 +2,11 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/pair.dart'; -import 'package:stackwallet/pages/buy_view/buy_coin_selection/crypto_selection_view.dart'; +import 'package:stackwallet/pages/buy_view/sub_widgets/crypto_selection_view.dart'; import 'package:stackwallet/pages/buy_view/sub_widgets/fiat_crypto_toggle.dart'; +import 'package:stackwallet/pages/buy_view/sub_widgets/fiat_selection_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -57,12 +59,6 @@ class _BuyFormState extends ConsumerState { } } - void selectFiatCrypto() async { - // await Future.delayed(const Duration(milliseconds: 300)); - // - // Navigator.of(context, rootNavigator: true).pop(); - } - void cryptoFieldOnChanged(String value) async { if (_cryptoFocusNode.hasFocus) { final newCryptoAmount = Decimal.tryParse(value); @@ -80,8 +76,8 @@ class _BuyFormState extends ConsumerState { } } - void selectCryptoCrypto() async { - final fromTicker = ref.read(exchangeFormStateProvider).fromTicker ?? "-"; + void selectCrypto() async { + final fromTicker = ref.read(buyFormStateProvider).fromTicker ?? "-"; // ref.read(estimatedRateExchangeFormProvider).from?.ticker ?? "-"; // if (walletInitiated && @@ -91,18 +87,18 @@ class _BuyFormState extends ConsumerState { // } List coins; - switch (ref.read(currentExchangeNameStateProvider.state).state) { - // case ChangeNowExchange.exchangeName: - // coins = ref.read(availableChangeNowCurrenciesProvider).coins; - // break; - // case SimpleSwapExchange.exchangeName: - // coins = ref - // .read(availableSimpleswapCurrenciesProvider) - // .floatingRateCurrencies; - // break; - default: - coins = []; - } + // switch (ref.read(currentExchangeNameStateProvider.state).state) { + // // case ChangeNowExchange.exchangeName: + // // coins = ref.read(availableChangeNowCurrenciesProvider).coins; + // // break; + // // case SimpleSwapExchange.exchangeName: + // // coins = ref + // // .read(availableSimpleswapCurrenciesProvider) + // // .floatingRateCurrencies; + // // break; + // default: + coins = []; + // } await _showFloatingCryptoSelectionSheet( coins: coins, @@ -251,6 +247,146 @@ class _BuyFormState extends ConsumerState { } } + void selectFiat() async { + final fromTicker = ref.read(buyFormStateProvider).fromTicker ?? "-"; + // ref.read(estimatedRateExchangeFormProvider).from?.ticker ?? "-"; + + // if (walletInitiated && + // fromTicker.toLowerCase() == coin!.ticker.toLowerCase()) { + // // do not allow changing away from wallet coin + // return; + // } + + List coins; + // switch (ref.read(currentExchangeNameStateProvider.state).state) { + // // case ChangeNowExchange.exchangeName: + // // coins = ref.read(availableChangeNowCurrenciesProvider).coins; + // // break; + // // case SimpleSwapExchange.exchangeName: + // // coins = ref + // // .read(availableSimpleswapCurrenciesProvider) + // // .floatingRateCurrencies; + // // break; + // default: + coins = []; + // } + } + + Future _showFloatingFiatSelectionSheet({ + required List fiats, + required String excludedTicker, + required String fromTicker, + required void Function(Fiat) onSelected, + }) async { + _fiatFocusNode.unfocus(); + _cryptoFocusNode.unfocus(); + + List allPairs; + + // switch (ref.read(currentExchangeNameStateProvider.state).state) { + // // case ChangeNowExchange.exchangeName: + // // allPairs = ref.read(availableChangeNowCurrenciesProvider).pairs; + // // break; + // // case SimpleSwapExchange.exchangeName: + // // allPairs = ref.read(exchangeFormStateProvider).exchangeType == + // // ExchangeRateType.fixed + // // ? ref.read(availableSimpleswapCurrenciesProvider).fixedRatePairs + // // : ref.read(availableSimpleswapCurrenciesProvider).floatingRatePairs; + // // break; + // default: + allPairs = []; + // } + + List availablePairs; + if (fromTicker.isEmpty || + fromTicker == "-" || + excludedTicker.isEmpty || + excludedTicker == "-") { + availablePairs = allPairs; + } else if (excludedTicker == fromTicker) { + availablePairs = allPairs + .where((e) => e.from == excludedTicker) + .toList(growable: false); + } else { + availablePairs = + allPairs.where((e) => e.to == excludedTicker).toList(growable: false); + } + + final List tickers = fiats.where((e) { + if (excludedTicker == fromTicker) { + return e.ticker != excludedTicker && + availablePairs.where((e2) => e2.to == e.ticker).isNotEmpty; + } else { + return e.ticker != excludedTicker && + availablePairs.where((e2) => e2.from == e.ticker).isNotEmpty; + } + }).toList(growable: false); + + 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( + coins: tickers, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + }) + : await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => FiatSelectionView( + coins: tickers, + ), + ), + ); + + if (mounted && result is Fiat) { + onSelected(result); + } + } + String? _fetchIconUrlFromTicker(String? ticker) { if (ticker == null) return null; @@ -364,13 +500,16 @@ class _BuyFormState extends ConsumerState { } }, onChanged: cryptoFieldOnChanged, - onButtonTap: selectCryptoCrypto, + onButtonTap: selectCrypto, isWalletCoin: isWalletCoin(coin, true), image: _fetchIconUrlFromTicker(ref .watch(buyFormStateProvider.select((value) => value.fromTicker))), ticker: ref .watch(buyFormStateProvider.select((value) => value.fromTicker)), ), + SizedBox( + height: isDesktop ? 20 : 12, + ), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -386,6 +525,33 @@ class _BuyFormState extends ConsumerState { SizedBox( height: isDesktop ? 10 : 4, ), + BuyTextField( + controller: _fiatController, + focusNode: _fiatFocusNode, + textStyle: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + buttonColor: + Theme.of(context).extension()!.buttonBackSecondary, + borderRadius: Constants.size.circularBorderRadius, + background: + Theme.of(context).extension()!.textFieldDefaultBG, + onTap: () { + if (_fiatController.text == "-") { + _fiatController.text = ""; + } + }, + onChanged: fiatFieldOnChanged, + onButtonTap: selectFiat, + isWalletCoin: isWalletCoin(coin, true), + image: _fetchIconUrlFromTicker(ref + .watch(buyFormStateProvider.select((value) => value.fromTicker))), + ticker: ref + .watch(buyFormStateProvider.select((value) => value.fromTicker)), + ), + SizedBox( + height: isDesktop ? 20 : 12, + ), Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -422,7 +588,7 @@ class _BuyFormState extends ConsumerState { } }, onChanged: fiatFieldOnChanged, - onButtonTap: selectFiatCrypto, + onButtonTap: selectFiat, // isWalletCoin: isWalletCoin(coin, true), isWalletCoin: false, // image: _fetchIconUrlFromTicker(ref diff --git a/lib/pages/buy_view/buy_coin_selection/crypto_selection_view.dart b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart similarity index 100% rename from lib/pages/buy_view/buy_coin_selection/crypto_selection_view.dart rename to lib/pages/buy_view/sub_widgets/crypto_selection_view.dart 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..001317cb6 --- /dev/null +++ b/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart @@ -0,0 +1,332 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.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/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/loading_indicator.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.coins, + }) : super(key: key); + + final List coins; + + @override + State createState() => _FiatSelectionViewState(); +} + +class _FiatSelectionViewState 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 crypto 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( + "Popular coins", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 12, + ), + Flexible( + child: Builder(builder: (context) { + final items = _coins + .where((e) => Coin.values + .where((coin) => + coin.ticker.toLowerCase() == e.ticker.toLowerCase()) + .isNotEmpty) + .toList(growable: false); + + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: ListView.builder( + shrinkWrap: true, + primary: isDesktop ? false : null, + itemCount: items.length, + itemBuilder: (builderContext, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: GestureDetector( + onTap: () { + Navigator.of(context).pop(items[index]); + }, + child: RoundedWhiteContainer( + child: Row( + children: [ + SizedBox( + width: 24, + height: 24, + child: SvgPicture.network( + items[index].image, + width: 24, + height: 24, + placeholderBuilder: (_) => + const LoadingIndicator(), + ), + ), + const SizedBox( + width: 10, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + items[index].name, + style: STextStyles.largeMedium14(context), + ), + const SizedBox( + height: 2, + ), + Text( + items[index].ticker.toUpperCase(), + style: STextStyles.smallMed12(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + }), + ), + const SizedBox( + height: 20, + ), + 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: SvgPicture.network( + _coins[index].image, + width: 24, + height: 24, + placeholderBuilder: (_) => + const LoadingIndicator(), + ), + ), + 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, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +}