add fiat selection view

This commit is contained in:
sneurlax 2023-01-11 11:49:59 -06:00
parent 9724b40848
commit 2d6447eedd
4 changed files with 644 additions and 23 deletions

View file

@ -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<String, dynamic> 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<String, dynamic> 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()}";
}
}

View file

@ -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<BuyForm> {
}
}
void selectFiatCrypto() async {
// await Future<void>.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<BuyForm> {
}
}
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<BuyForm> {
// }
List<Crypto> 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<BuyForm> {
}
}
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<Crypto> 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<void> _showFloatingFiatSelectionSheet({
required List<Fiat> fiats,
required String excludedTicker,
required String fromTicker,
required void Function(Fiat) onSelected,
}) async {
_fiatFocusNode.unfocus();
_cryptoFocusNode.unfocus();
List<Pair> 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<Pair> 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<Fiat> 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<Crypto?>(
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<StackColors>()!
.background,
child: FiatSelectionView(
coins: tickers,
),
),
),
],
),
),
),
],
),
);
})
: await Navigator.of(context).push(
MaterialPageRoute<dynamic>(
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<BuyForm> {
}
},
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<BuyForm> {
SizedBox(
height: isDesktop ? 10 : 4,
),
BuyTextField(
controller: _fiatController,
focusNode: _fiatFocusNode,
textStyle: STextStyles.smallMed14(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark,
),
buttonColor:
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
borderRadius: Constants.size.circularBorderRadius,
background:
Theme.of(context).extension<StackColors>()!.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<BuyForm> {
}
},
onChanged: fiatFieldOnChanged,
onButtonTap: selectFiatCrypto,
onButtonTap: selectFiat,
// isWalletCoin: isWalletCoin(coin, true),
isWalletCoin: false,
// image: _fetchIconUrlFromTicker(ref

View file

@ -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<Fiat> coins;
@override
State<FiatSelectionView> createState() => _FiatSelectionViewState();
}
class _FiatSelectionViewState extends State<FiatSelectionView> {
late TextEditingController _searchController;
final _searchFocusNode = FocusNode();
late final List<Fiat> coins;
late List<Fiat> _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<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
if (FocusScope.of(context).hasFocus) {
FocusScope.of(context).unfocus();
await Future<void>.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<StackColors>()!
.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<StackColors>()!
.textSubtitle1,
),
),
],
),
),
],
),
),
),
);
},
),
),
),
],
),
);
}
}