mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 20:09:23 +00:00
1698 lines
81 KiB
Dart
1698 lines
81 KiB
Dart
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/flutter_svg.dart';
|
|
import 'package:flutter_svg/svg.dart';
|
|
import 'package:stackwallet/models/exchange/change_now/available_floating_rate_pair.dart';
|
|
import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart';
|
|
import 'package:stackwallet/models/exchange/change_now/currency.dart';
|
|
import 'package:stackwallet/models/exchange/change_now/fixed_rate_market.dart';
|
|
import 'package:stackwallet/models/exchange/incomplete_exchange.dart';
|
|
import 'package:stackwallet/notifications/show_flush_bar.dart';
|
|
import 'package:stackwallet/pages/exchange_view/exchange_coin_selection/fixed_rate_pair_coin_selection_view.dart';
|
|
import 'package:stackwallet/pages/exchange_view/exchange_coin_selection/floating_rate_currency_selection_view.dart';
|
|
import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_1_view.dart';
|
|
import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart';
|
|
import 'package:stackwallet/pages/exchange_view/trade_details_view.dart';
|
|
import 'package:stackwallet/providers/exchange/available_currencies_state_provider.dart';
|
|
import 'package:stackwallet/providers/exchange/available_floating_rate_pairs_state_provider.dart';
|
|
import 'package:stackwallet/providers/exchange/change_now_provider.dart';
|
|
import 'package:stackwallet/providers/exchange/changenow_initial_load_status.dart';
|
|
import 'package:stackwallet/providers/exchange/estimate_rate_exchange_form_provider.dart';
|
|
import 'package:stackwallet/providers/exchange/exchange_send_from_wallet_id_provider.dart';
|
|
import 'package:stackwallet/providers/exchange/fixed_rate_exchange_form_provider.dart';
|
|
import 'package:stackwallet/providers/exchange/fixed_rate_market_pairs_provider.dart';
|
|
import 'package:stackwallet/providers/exchange/trade_sent_from_stack_lookup_provider.dart';
|
|
import 'package:stackwallet/providers/global/trades_service_provider.dart';
|
|
import 'package:stackwallet/providers/providers.dart';
|
|
import 'package:stackwallet/utilities/assets.dart';
|
|
import 'package:stackwallet/utilities/constants.dart';
|
|
import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
|
|
import 'package:stackwallet/utilities/text_styles.dart';
|
|
import 'package:stackwallet/utilities/theme/stack_colors.dart';
|
|
import 'package:stackwallet/widgets/custom_loading_overlay.dart';
|
|
import 'package:stackwallet/widgets/loading_indicator.dart';
|
|
import 'package:stackwallet/widgets/stack_dialog.dart';
|
|
import 'package:stackwallet/widgets/trade_card.dart';
|
|
import 'package:tuple/tuple.dart';
|
|
|
|
const kFixedRateEnabled = true;
|
|
|
|
class ExchangeView extends ConsumerStatefulWidget {
|
|
const ExchangeView({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
ConsumerState<ExchangeView> createState() => _ExchangeViewState();
|
|
}
|
|
|
|
class _ExchangeViewState extends ConsumerState<ExchangeView> {
|
|
late final TextEditingController _sendController;
|
|
late final TextEditingController _receiveController;
|
|
|
|
bool _swapLock = false;
|
|
|
|
Future<void> _swap() async {
|
|
_swapLock = true;
|
|
_sendFocusNode.unfocus();
|
|
_receiveFocusNode.unfocus();
|
|
|
|
unawaited(
|
|
showDialog<void>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (_) => WillPopScope(
|
|
onWillPop: () async => false,
|
|
child: Container(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.overlay
|
|
.withOpacity(0.6),
|
|
child: const CustomLoadingOverlay(
|
|
message: "Updating exchange rate",
|
|
eventBus: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
if (ref.watch(prefsChangeNotifierProvider
|
|
.select((pref) => pref.exchangeRateType)) ==
|
|
ExchangeRateType.estimated) {
|
|
await ref.read(estimatedRateExchangeFormProvider).swap();
|
|
} else {
|
|
final from = ref.read(fixedRateExchangeFormProvider).market?.from;
|
|
final to = ref.read(fixedRateExchangeFormProvider).market?.to;
|
|
|
|
if (to != null && from != null) {
|
|
final markets = ref
|
|
.read(fixedRateMarketPairsStateProvider.state)
|
|
.state
|
|
.where((e) => e.from == to && e.to == from);
|
|
|
|
if (markets.isNotEmpty) {
|
|
await ref.read(fixedRateExchangeFormProvider).swap(markets.first);
|
|
}
|
|
}
|
|
}
|
|
if (mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
_swapLock = false;
|
|
}
|
|
|
|
Future<void> _showFloatingRateSelectionSheet({
|
|
required List<Currency> currencies,
|
|
required String excludedTicker,
|
|
required String fromTicker,
|
|
required void Function(Currency) onSelected,
|
|
}) async {
|
|
_sendFocusNode.unfocus();
|
|
_receiveFocusNode.unfocus();
|
|
|
|
List<AvailableFloatingRatePair> availablePairs = [];
|
|
if (fromTicker.isEmpty ||
|
|
fromTicker == "-" ||
|
|
excludedTicker.isEmpty ||
|
|
excludedTicker == "-") {
|
|
availablePairs =
|
|
ref.read(availableFloatingRatePairsStateProvider.state).state;
|
|
} else if (excludedTicker == fromTicker) {
|
|
availablePairs = ref
|
|
.read(availableFloatingRatePairsStateProvider.state)
|
|
.state
|
|
.where((e) => e.fromTicker == excludedTicker)
|
|
.toList(growable: false);
|
|
} else {
|
|
availablePairs = ref
|
|
.read(availableFloatingRatePairsStateProvider.state)
|
|
.state
|
|
.where((e) => e.toTicker == excludedTicker)
|
|
.toList(growable: false);
|
|
}
|
|
|
|
final List<Currency> tickers = currencies.where((e) {
|
|
if (excludedTicker == fromTicker) {
|
|
return e.ticker != excludedTicker &&
|
|
availablePairs.where((e2) => e2.toTicker == e.ticker).isNotEmpty;
|
|
} else {
|
|
return e.ticker != excludedTicker &&
|
|
availablePairs.where((e2) => e2.fromTicker == e.ticker).isNotEmpty;
|
|
}
|
|
}).toList(growable: false);
|
|
|
|
final result = await Navigator.of(context).push(
|
|
MaterialPageRoute<dynamic>(
|
|
builder: (_) => FloatingRateCurrencySelectionView(
|
|
currencies: tickers,
|
|
),
|
|
),
|
|
);
|
|
|
|
if (mounted && result is Currency) {
|
|
onSelected(result);
|
|
}
|
|
}
|
|
|
|
String? _fetchIconUrlFromTickerForFixedRateFlow(String? ticker) {
|
|
if (ticker == null) return null;
|
|
|
|
final possibleCurrencies = ref
|
|
.read(availableChangeNowCurrenciesStateProvider.state)
|
|
.state
|
|
.where((e) => e.ticker.toUpperCase() == ticker.toUpperCase());
|
|
|
|
for (final currency in possibleCurrencies) {
|
|
if (currency.image.isNotEmpty) {
|
|
return currency.image;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
Future<void> _showFixedRateSelectionSheet({
|
|
required String excludedTicker,
|
|
required String fromTicker,
|
|
required void Function(String) onSelected,
|
|
}) async {
|
|
_sendFocusNode.unfocus();
|
|
_receiveFocusNode.unfocus();
|
|
|
|
List<FixedRateMarket> marketsThatPairWithExcludedTicker = [];
|
|
|
|
if (excludedTicker == "" ||
|
|
excludedTicker == "-" ||
|
|
fromTicker == "" ||
|
|
fromTicker == "-") {
|
|
marketsThatPairWithExcludedTicker =
|
|
ref.read(fixedRateMarketPairsStateProvider.state).state;
|
|
} else if (excludedTicker == fromTicker) {
|
|
marketsThatPairWithExcludedTicker = ref
|
|
.read(fixedRateMarketPairsStateProvider.state)
|
|
.state
|
|
.where((e) => e.from == excludedTicker && e.to != excludedTicker)
|
|
.toList(growable: false);
|
|
} else {
|
|
marketsThatPairWithExcludedTicker = ref
|
|
.read(fixedRateMarketPairsStateProvider.state)
|
|
.state
|
|
.where((e) => e.to == excludedTicker && e.from != excludedTicker)
|
|
.toList(growable: false);
|
|
}
|
|
|
|
final result = await Navigator.of(context).push(
|
|
MaterialPageRoute<dynamic>(
|
|
builder: (_) => FixedRateMarketPairCoinSelectionView(
|
|
markets: marketsThatPairWithExcludedTicker,
|
|
currencies:
|
|
ref.read(availableChangeNowCurrenciesStateProvider.state).state,
|
|
isFrom: excludedTicker != fromTicker,
|
|
),
|
|
),
|
|
);
|
|
|
|
if (mounted && result is String) {
|
|
onSelected(result);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
_sendController = TextEditingController();
|
|
_receiveController = TextEditingController();
|
|
|
|
final isEstimated =
|
|
ref.read(prefsChangeNotifierProvider).exchangeRateType ==
|
|
ExchangeRateType.estimated;
|
|
_sendController.text = isEstimated
|
|
? ref.read(estimatedRateExchangeFormProvider).fromAmountString
|
|
: ref.read(fixedRateExchangeFormProvider).fromAmountString;
|
|
_receiveController.text = isEstimated
|
|
? ref.read(estimatedRateExchangeFormProvider).toAmountString
|
|
: ref.read(fixedRateExchangeFormProvider).toAmountString;
|
|
|
|
_sendFocusNode.addListener(() async {
|
|
if (!_sendFocusNode.hasFocus) {
|
|
final newFromAmount = Decimal.tryParse(_sendController.text);
|
|
if (newFromAmount != null) {
|
|
if (ref.read(prefsChangeNotifierProvider).exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
await ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(newFromAmount, true);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(newFromAmount, true);
|
|
}
|
|
} else {
|
|
if (ref.read(prefsChangeNotifierProvider).exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
await ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(Decimal.zero, true);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(Decimal.zero, true);
|
|
}
|
|
_receiveController.text = "";
|
|
}
|
|
}
|
|
});
|
|
_receiveFocusNode.addListener(() async {
|
|
if (!_receiveFocusNode.hasFocus) {
|
|
final newToAmount = Decimal.tryParse(_receiveController.text);
|
|
if (newToAmount != null) {
|
|
if (ref.read(prefsChangeNotifierProvider).exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
// await ref
|
|
// .read(estimatedRateExchangeFormProvider)
|
|
// .setToAmountAndCalculateFromAmount(newToAmount, true);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setToAmountAndCalculateFromAmount(newToAmount, true);
|
|
}
|
|
} else {
|
|
if (ref.read(prefsChangeNotifierProvider).exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
// await ref
|
|
// .read(estimatedRateExchangeFormProvider)
|
|
// .setToAmountAndCalculateFromAmount(Decimal.zero, true);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setToAmountAndCalculateFromAmount(Decimal.zero, true);
|
|
}
|
|
_sendController.text = "";
|
|
}
|
|
}
|
|
});
|
|
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_receiveController.dispose();
|
|
_sendController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
String calcSend = "";
|
|
String calcReceive = "";
|
|
|
|
final FocusNode _sendFocusNode = FocusNode();
|
|
final FocusNode _receiveFocusNode = FocusNode();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
debugPrint("BUILD: $runtimeType");
|
|
|
|
final isEstimated = ref.watch(prefsChangeNotifierProvider
|
|
.select((pref) => pref.exchangeRateType)) ==
|
|
ExchangeRateType.estimated;
|
|
|
|
ref.listen(
|
|
isEstimated
|
|
? estimatedRateExchangeFormProvider
|
|
.select((value) => value.toAmountString)
|
|
: fixedRateExchangeFormProvider.select(
|
|
(value) => value.toAmountString), (previous, String next) {
|
|
if (!_receiveFocusNode.hasFocus) {
|
|
_receiveController.text = next;
|
|
debugPrint("RECEIVE AMOUNT LISTENER ACTIVATED");
|
|
if (_swapLock) {
|
|
_sendController.text = isEstimated
|
|
? ref.read(estimatedRateExchangeFormProvider).fromAmountString
|
|
: ref.read(fixedRateExchangeFormProvider).fromAmountString;
|
|
}
|
|
}
|
|
});
|
|
ref.listen(
|
|
isEstimated
|
|
? estimatedRateExchangeFormProvider
|
|
.select((value) => value.fromAmountString)
|
|
: fixedRateExchangeFormProvider.select(
|
|
(value) => value.fromAmountString), (previous, String next) {
|
|
if (!_sendFocusNode.hasFocus) {
|
|
_sendController.text = next;
|
|
debugPrint("SEND AMOUNT LISTENER ACTIVATED");
|
|
if (_swapLock) {
|
|
_receiveController.text = isEstimated
|
|
? ref.read(estimatedRateExchangeFormProvider).toAmountString
|
|
: ref.read(fixedRateExchangeFormProvider).toAmountString;
|
|
}
|
|
}
|
|
});
|
|
|
|
return SafeArea(
|
|
child: NestedScrollView(
|
|
floatHeaderSlivers: true,
|
|
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
|
return [
|
|
SliverOverlapAbsorber(
|
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context),
|
|
sliver: SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const SizedBox(
|
|
height: 20,
|
|
),
|
|
Text(
|
|
"You will send",
|
|
style: STextStyles.itemSubtitle(context).copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark3,
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 4,
|
|
),
|
|
TextFormField(
|
|
style: STextStyles.smallMed14(context).copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark,
|
|
),
|
|
focusNode: _sendFocusNode,
|
|
controller: _sendController,
|
|
textAlign: TextAlign.right,
|
|
onTap: () {
|
|
if (_sendController.text == "-") {
|
|
_sendController.text = "";
|
|
}
|
|
},
|
|
onChanged: (value) async {
|
|
final newFromAmount = Decimal.tryParse(value);
|
|
if (newFromAmount != null) {
|
|
if (ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
await ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
newFromAmount, false);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
newFromAmount, false);
|
|
}
|
|
} else {
|
|
if (ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
await ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
Decimal.zero, false);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
Decimal.zero, false);
|
|
}
|
|
_receiveController.text = "";
|
|
}
|
|
},
|
|
keyboardType: const TextInputType.numberWithOptions(
|
|
signed: false,
|
|
decimal: true,
|
|
),
|
|
inputFormatters: [
|
|
// regex to validate a crypto amount with 8 decimal places
|
|
TextInputFormatter.withFunction((oldValue,
|
|
newValue) =>
|
|
RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
|
|
.hasMatch(newValue.text)
|
|
? newValue
|
|
: oldValue),
|
|
],
|
|
decoration: InputDecoration(
|
|
contentPadding: const EdgeInsets.only(
|
|
top: 12,
|
|
right: 12,
|
|
),
|
|
hintText: "0",
|
|
hintStyle: STextStyles.fieldLabel(context).copyWith(
|
|
fontSize: 14,
|
|
),
|
|
prefixIcon: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
alignment: Alignment.centerLeft,
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
if (ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
await _showFloatingRateSelectionSheet(
|
|
currencies: ref
|
|
.read(
|
|
availableChangeNowCurrenciesStateProvider
|
|
.state)
|
|
.state,
|
|
excludedTicker: ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.to
|
|
?.ticker ??
|
|
"-",
|
|
fromTicker: ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.from
|
|
?.ticker ??
|
|
"-",
|
|
onSelected: (from) => ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.updateFrom(from, true));
|
|
} else {
|
|
final toTicker = ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.market
|
|
?.to ??
|
|
"";
|
|
final fromTicker = ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.market
|
|
?.from ??
|
|
"";
|
|
await _showFixedRateSelectionSheet(
|
|
excludedTicker: toTicker,
|
|
fromTicker: fromTicker,
|
|
onSelected: (selectedFromTicker) async {
|
|
try {
|
|
final market = ref
|
|
.read(
|
|
fixedRateMarketPairsStateProvider
|
|
.state)
|
|
.state
|
|
.firstWhere(
|
|
(e) =>
|
|
e.to == toTicker &&
|
|
e.from == selectedFromTicker,
|
|
);
|
|
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.updateMarket(market, true);
|
|
} catch (e) {
|
|
unawaited(showDialog<dynamic>(
|
|
context: context,
|
|
builder: (_) => const StackDialog(
|
|
title: "Fixed rate market error",
|
|
message:
|
|
"Could not find the specified fixed rate trade pair",
|
|
),
|
|
));
|
|
return;
|
|
}
|
|
},
|
|
);
|
|
}
|
|
},
|
|
child: Container(
|
|
color: Colors.transparent,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 18,
|
|
height: 18,
|
|
decoration: BoxDecoration(
|
|
borderRadius:
|
|
BorderRadius.circular(18),
|
|
),
|
|
child: Builder(
|
|
builder: (context) {
|
|
String? image;
|
|
if (ref.watch(
|
|
prefsChangeNotifierProvider
|
|
.select((value) => value
|
|
.exchangeRateType)) ==
|
|
ExchangeRateType.estimated) {
|
|
image = ref
|
|
.watch(
|
|
estimatedRateExchangeFormProvider
|
|
.select((value) =>
|
|
value.from))
|
|
?.image;
|
|
} else {
|
|
image = _fetchIconUrlFromTickerForFixedRateFlow(
|
|
ref.watch(
|
|
fixedRateExchangeFormProvider
|
|
.select((value) =>
|
|
value.market
|
|
?.from)));
|
|
}
|
|
if (image != null &&
|
|
image.isNotEmpty) {
|
|
return Center(
|
|
child: SvgPicture.network(
|
|
image,
|
|
height: 18,
|
|
placeholderBuilder: (_) =>
|
|
Container(
|
|
width: 18,
|
|
height: 18,
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context)
|
|
.extension<
|
|
StackColors>()!
|
|
.textFieldDefaultBG,
|
|
borderRadius:
|
|
BorderRadius.circular(
|
|
18,
|
|
),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius:
|
|
BorderRadius.circular(
|
|
18,
|
|
),
|
|
child:
|
|
const LoadingIndicator(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
return Container(
|
|
width: 18,
|
|
height: 18,
|
|
decoration: BoxDecoration(
|
|
// color: Theme.of(context).extension<StackColors>()!.accentColorDark
|
|
borderRadius:
|
|
BorderRadius.circular(18),
|
|
),
|
|
child: SvgPicture.asset(
|
|
Assets.svg.circleQuestion,
|
|
width: 18,
|
|
height: 18,
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textFieldDefaultBG,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 6,
|
|
),
|
|
Text(
|
|
isEstimated
|
|
? ref.watch(
|
|
estimatedRateExchangeFormProvider
|
|
.select((value) => value
|
|
.from?.ticker
|
|
.toUpperCase())) ??
|
|
"-"
|
|
: ref.watch(
|
|
fixedRateExchangeFormProvider
|
|
.select((value) => value
|
|
.market?.from
|
|
.toUpperCase())) ??
|
|
"-",
|
|
style: STextStyles.smallMed14(context)
|
|
.copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark,
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 6,
|
|
),
|
|
SvgPicture.asset(
|
|
Assets.svg.chevronDown,
|
|
width: 5,
|
|
height: 2.5,
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 4,
|
|
),
|
|
Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Align(
|
|
alignment: Alignment.bottomLeft,
|
|
child: Text(
|
|
"You will receive",
|
|
style:
|
|
STextStyles.itemSubtitle(context).copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark3,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Center(
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(
|
|
height: 6,
|
|
),
|
|
GestureDetector(
|
|
onTap: () async {
|
|
await _swap();
|
|
},
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(4),
|
|
child: SvgPicture.asset(
|
|
Assets.svg.swap,
|
|
width: 20,
|
|
height: 20,
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.accentColorDark,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 6,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: Align(
|
|
alignment: Alignment.topRight,
|
|
child: Text(
|
|
isEstimated
|
|
? ref.watch(
|
|
estimatedRateExchangeFormProvider
|
|
.select((value) =>
|
|
value.minimumSendWarning))
|
|
: ref.watch(fixedRateExchangeFormProvider
|
|
.select((value) =>
|
|
value.sendAmountWarning)),
|
|
style: STextStyles.errorSmall(context),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(
|
|
height: 4,
|
|
),
|
|
TextFormField(
|
|
style: STextStyles.smallMed14(context).copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark,
|
|
),
|
|
focusNode: _receiveFocusNode,
|
|
controller: _receiveController,
|
|
readOnly: ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated,
|
|
onTap: () {
|
|
if (_receiveController.text == "-") {
|
|
_receiveController.text = "";
|
|
}
|
|
},
|
|
onChanged: (value) async {
|
|
final newToAmount = Decimal.tryParse(value);
|
|
if (newToAmount != null) {
|
|
if (ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
// await ref
|
|
// .read(estimatedRateExchangeFormProvider)
|
|
// .setToAmountAndCalculateFromAmount(
|
|
// newToAmount, false);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setToAmountAndCalculateFromAmount(
|
|
newToAmount, false);
|
|
}
|
|
} else {
|
|
if (ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
// await ref
|
|
// .read(estimatedRateExchangeFormProvider)
|
|
// .setToAmountAndCalculateFromAmount(
|
|
// Decimal.zero, false);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setToAmountAndCalculateFromAmount(
|
|
Decimal.zero, false);
|
|
}
|
|
_sendController.text = "";
|
|
}
|
|
},
|
|
textAlign: TextAlign.right,
|
|
keyboardType: const TextInputType.numberWithOptions(
|
|
signed: false,
|
|
decimal: true,
|
|
),
|
|
inputFormatters: [
|
|
// regex to validate a crypto amount with 8 decimal places
|
|
TextInputFormatter.withFunction((oldValue,
|
|
newValue) =>
|
|
RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
|
|
.hasMatch(newValue.text)
|
|
? newValue
|
|
: oldValue),
|
|
],
|
|
decoration: InputDecoration(
|
|
contentPadding: const EdgeInsets.only(
|
|
top: 12,
|
|
right: 12,
|
|
),
|
|
hintText: "0",
|
|
hintStyle: STextStyles.fieldLabel(context).copyWith(
|
|
fontSize: 14,
|
|
),
|
|
prefixIcon: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: GestureDetector(
|
|
onTap: () async {
|
|
if (ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
await _showFloatingRateSelectionSheet(
|
|
currencies: ref
|
|
.read(
|
|
availableChangeNowCurrenciesStateProvider
|
|
.state)
|
|
.state,
|
|
excludedTicker: ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.from
|
|
?.ticker ??
|
|
"",
|
|
fromTicker: ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.from
|
|
?.ticker ??
|
|
"",
|
|
onSelected: (to) => ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.updateTo(to, true));
|
|
} else {
|
|
final fromTicker = ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.market
|
|
?.from ??
|
|
"";
|
|
await _showFixedRateSelectionSheet(
|
|
excludedTicker: fromTicker,
|
|
fromTicker: fromTicker,
|
|
onSelected: (selectedToTicker) async {
|
|
try {
|
|
final market = ref
|
|
.read(
|
|
fixedRateMarketPairsStateProvider
|
|
.state)
|
|
.state
|
|
.firstWhere(
|
|
(e) =>
|
|
e.to == selectedToTicker &&
|
|
e.from == fromTicker,
|
|
);
|
|
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.updateMarket(market, true);
|
|
} catch (e) {
|
|
unawaited(showDialog<dynamic>(
|
|
context: context,
|
|
builder: (_) => const StackDialog(
|
|
title: "Fixed rate market error",
|
|
message:
|
|
"Could not find the specified fixed rate trade pair",
|
|
),
|
|
));
|
|
return;
|
|
}
|
|
},
|
|
);
|
|
}
|
|
},
|
|
child: Container(
|
|
color: Colors.transparent,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 18,
|
|
height: 18,
|
|
decoration: BoxDecoration(
|
|
borderRadius:
|
|
BorderRadius.circular(18),
|
|
),
|
|
child: Builder(
|
|
builder: (context) {
|
|
String? image;
|
|
if (ref.watch(
|
|
prefsChangeNotifierProvider
|
|
.select((value) => value
|
|
.exchangeRateType)) ==
|
|
ExchangeRateType.estimated) {
|
|
image = ref
|
|
.watch(
|
|
estimatedRateExchangeFormProvider
|
|
.select((value) =>
|
|
value.to))
|
|
?.image;
|
|
} else {
|
|
image = _fetchIconUrlFromTickerForFixedRateFlow(
|
|
ref.watch(
|
|
fixedRateExchangeFormProvider
|
|
.select((value) =>
|
|
value.market
|
|
?.to)));
|
|
}
|
|
if (image != null &&
|
|
image.isNotEmpty) {
|
|
return Center(
|
|
child: SvgPicture.network(
|
|
image,
|
|
height: 18,
|
|
placeholderBuilder: (_) =>
|
|
Container(
|
|
width: 18,
|
|
height: 18,
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context)
|
|
.extension<
|
|
StackColors>()!
|
|
.textFieldDefaultBG,
|
|
borderRadius:
|
|
BorderRadius.circular(
|
|
18),
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius:
|
|
BorderRadius.circular(
|
|
18,
|
|
),
|
|
child:
|
|
const LoadingIndicator(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
} else {
|
|
return Container(
|
|
width: 18,
|
|
height: 18,
|
|
decoration: BoxDecoration(
|
|
// color: Theme.of(context).extension<StackColors>()!.accentColorDark
|
|
borderRadius:
|
|
BorderRadius.circular(18),
|
|
),
|
|
child: SvgPicture.asset(
|
|
Assets.svg.circleQuestion,
|
|
width: 18,
|
|
height: 18,
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textFieldDefaultBG,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 6,
|
|
),
|
|
Text(
|
|
isEstimated
|
|
? ref.watch(
|
|
estimatedRateExchangeFormProvider
|
|
.select((value) => value
|
|
.to?.ticker
|
|
.toUpperCase())) ??
|
|
"-"
|
|
: ref.watch(
|
|
fixedRateExchangeFormProvider
|
|
.select((value) => value
|
|
.market?.to
|
|
.toUpperCase())) ??
|
|
"-",
|
|
style: STextStyles.smallMed14(context)
|
|
.copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark,
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 6,
|
|
),
|
|
SvgPicture.asset(
|
|
Assets.svg.chevronDown,
|
|
width: 5,
|
|
height: 2.5,
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// if (ref
|
|
// .watch(exchangeFormSateProvider
|
|
// .select((value) => value.minimumReceiveWarning))
|
|
// .isNotEmpty)
|
|
// SizedBox(
|
|
// height: 4,
|
|
// ),
|
|
//
|
|
// if (ref
|
|
// .watch(exchangeFormSateProvider
|
|
// .select((value) => value.minimumReceiveWarning))
|
|
// .isNotEmpty)
|
|
// Row(
|
|
// children: [
|
|
// Spacer(),
|
|
// Text(
|
|
// ref.watch(exchangeFormSateProvider.select(
|
|
// (value) => value.minimumReceiveWarning)),
|
|
// style: STextStyles.errorSmall(context),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
|
|
const SizedBox(
|
|
height: 12,
|
|
),
|
|
RateInfo(
|
|
onChanged: (rateType) async {
|
|
_receiveFocusNode.unfocus();
|
|
_sendFocusNode.unfocus();
|
|
switch (rateType) {
|
|
case ExchangeRateType.estimated:
|
|
final market = ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.market;
|
|
final fromTicker = market?.from ?? "";
|
|
final toTicker = market?.to ?? "";
|
|
if (!(fromTicker.isEmpty ||
|
|
toTicker.isEmpty ||
|
|
toTicker == "-" ||
|
|
fromTicker == "-")) {
|
|
final available = ref
|
|
.read(
|
|
availableFloatingRatePairsStateProvider
|
|
.state)
|
|
.state
|
|
.where((e) =>
|
|
e.toTicker == toTicker &&
|
|
e.fromTicker == fromTicker);
|
|
if (available.isNotEmpty) {
|
|
final availableCurrencies = ref
|
|
.read(
|
|
availableChangeNowCurrenciesStateProvider
|
|
.state)
|
|
.state
|
|
.where((e) =>
|
|
e.ticker == fromTicker ||
|
|
e.ticker == toTicker);
|
|
if (availableCurrencies.length > 1) {
|
|
final from = availableCurrencies.firstWhere(
|
|
(e) => e.ticker == fromTicker);
|
|
final to = availableCurrencies.firstWhere(
|
|
(e) => e.ticker == toTicker);
|
|
|
|
final newFromAmount =
|
|
Decimal.tryParse(_sendController.text);
|
|
if (newFromAmount != null) {
|
|
await ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
newFromAmount, false);
|
|
} else {
|
|
await ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
Decimal.zero, false);
|
|
|
|
_receiveController.text = "";
|
|
}
|
|
|
|
await ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.updateTo(to, false);
|
|
await ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.updateFrom(from, true);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
unawaited(showFloatingFlushBar(
|
|
type: FlushBarType.warning,
|
|
message:
|
|
"Estimated rate trade pair \"$fromTicker-$toTicker\" unavailable. Reverting to last estimated rate pair.",
|
|
context: context,
|
|
));
|
|
break;
|
|
case ExchangeRateType.fixed:
|
|
final fromTicker = ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.from
|
|
?.ticker ??
|
|
"";
|
|
final toTicker = ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.to
|
|
?.ticker ??
|
|
"";
|
|
if (!(fromTicker.isEmpty ||
|
|
toTicker.isEmpty ||
|
|
toTicker == "-" ||
|
|
fromTicker == "-")) {
|
|
FixedRateMarket? market;
|
|
try {
|
|
market = ref
|
|
.read(fixedRateMarketPairsStateProvider
|
|
.state)
|
|
.state
|
|
.firstWhere((e) =>
|
|
e.from == fromTicker &&
|
|
e.to == toTicker);
|
|
} catch (_) {
|
|
market = null;
|
|
}
|
|
|
|
final newFromAmount =
|
|
Decimal.tryParse(_sendController.text);
|
|
if (newFromAmount != null) {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
newFromAmount, false);
|
|
} else {
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
Decimal.zero, false);
|
|
|
|
_receiveController.text = "";
|
|
}
|
|
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.updateMarket(market, false);
|
|
await ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.setFromAmountAndCalculateToAmount(
|
|
Decimal.tryParse(_sendController.text) ??
|
|
Decimal.zero,
|
|
true,
|
|
);
|
|
return;
|
|
}
|
|
unawaited(showFloatingFlushBar(
|
|
type: FlushBarType.warning,
|
|
message:
|
|
"Fixed rate trade pair \"$fromTicker-$toTicker\" unavailable. Reverting to last fixed rate pair.",
|
|
context: context,
|
|
));
|
|
break;
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(
|
|
height: 12,
|
|
),
|
|
TextButton(
|
|
style: ((ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated)
|
|
? ref.watch(estimatedRateExchangeFormProvider
|
|
.select((value) => value.canExchange))
|
|
: ref.watch(fixedRateExchangeFormProvider
|
|
.select((value) => value.canExchange)))
|
|
? Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.getPrimaryEnabledButtonColor(context)
|
|
: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.getPrimaryDisabledButtonColor(context),
|
|
onPressed: ((ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated)
|
|
? ref.watch(estimatedRateExchangeFormProvider
|
|
.select((value) => value.canExchange))
|
|
: ref.watch(fixedRateExchangeFormProvider
|
|
.select((value) => value.canExchange)))
|
|
? () async {
|
|
if (ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType ==
|
|
ExchangeRateType.estimated) {
|
|
final fromTicker = ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.from
|
|
?.ticker ??
|
|
"";
|
|
final toTicker = ref
|
|
.read(
|
|
estimatedRateExchangeFormProvider)
|
|
.to
|
|
?.ticker ??
|
|
"";
|
|
|
|
bool isAvailable = false;
|
|
final availableFloatingPairs = ref
|
|
.read(
|
|
availableFloatingRatePairsStateProvider
|
|
.state)
|
|
.state;
|
|
for (final pair in availableFloatingPairs) {
|
|
if (pair.fromTicker == fromTicker &&
|
|
pair.toTicker == toTicker) {
|
|
isAvailable = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isAvailable) {
|
|
unawaited(showDialog<dynamic>(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (_) => StackDialog(
|
|
title:
|
|
"Selected trade pair unavailable",
|
|
message:
|
|
"The $fromTicker - $toTicker market is currently disabled for estimated/floating rate trades",
|
|
),
|
|
));
|
|
return;
|
|
}
|
|
|
|
final sendAmount = Decimal.parse(ref
|
|
.read(estimatedRateExchangeFormProvider)
|
|
.fromAmountString);
|
|
|
|
final rateType = ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType;
|
|
|
|
final response = await ref
|
|
.read(changeNowProvider)
|
|
.getEstimatedExchangeAmount(
|
|
fromTicker: fromTicker,
|
|
toTicker: toTicker,
|
|
fromAmount: sendAmount,
|
|
);
|
|
|
|
if (response.value == null) {
|
|
unawaited(showDialog<dynamic>(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (_) => StackDialog(
|
|
title:
|
|
"Failed to update trade estimate",
|
|
message: response.exception?.toString(),
|
|
),
|
|
));
|
|
return;
|
|
}
|
|
|
|
String rate =
|
|
"1 ${fromTicker.toUpperCase()} ~${(response.value!.estimatedAmount / sendAmount).toDecimal(scaleOnInfinitePrecision: 8).toStringAsFixed(8)} ${toTicker.toUpperCase()}";
|
|
|
|
final model = IncompleteExchangeModel(
|
|
sendTicker: fromTicker.toUpperCase(),
|
|
receiveTicker: toTicker.toUpperCase(),
|
|
rateInfo: rate,
|
|
sendAmount: sendAmount,
|
|
receiveAmount:
|
|
response.value!.estimatedAmount,
|
|
rateId: response.value!.rateId,
|
|
rateType: rateType,
|
|
);
|
|
|
|
if (mounted) {
|
|
ref
|
|
.read(
|
|
exchangeSendFromWalletIdStateProvider
|
|
.state)
|
|
.state = null;
|
|
unawaited(Navigator.of(context).pushNamed(
|
|
Step1View.routeName,
|
|
arguments: model,
|
|
));
|
|
}
|
|
} else {
|
|
final fromTicker = ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.market
|
|
?.from ??
|
|
"";
|
|
final toTicker = ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.market
|
|
?.to ??
|
|
"";
|
|
|
|
final sendAmount = Decimal.parse(ref
|
|
.read(fixedRateExchangeFormProvider)
|
|
.fromAmountString);
|
|
|
|
final rateType = ref
|
|
.read(prefsChangeNotifierProvider)
|
|
.exchangeRateType;
|
|
|
|
final response = await ref
|
|
.read(changeNowProvider)
|
|
.getEstimatedExchangeAmountV2(
|
|
fromTicker: fromTicker,
|
|
toTicker: toTicker,
|
|
fromOrTo: CNEstimateType.direct,
|
|
amount: sendAmount,
|
|
flow: CNFlowType.fixedRate,
|
|
);
|
|
|
|
bool? shouldCancel;
|
|
|
|
if (response.value == null) {
|
|
unawaited(showDialog<dynamic>(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (_) => StackDialog(
|
|
title:
|
|
"Failed to update trade estimate",
|
|
message: response.exception?.toString(),
|
|
),
|
|
));
|
|
return;
|
|
} else if (response.value!.warningMessage !=
|
|
null &&
|
|
response
|
|
.value!.warningMessage!.isNotEmpty) {
|
|
shouldCancel = await showDialog<bool?>(
|
|
context: context,
|
|
barrierDismissible: true,
|
|
builder: (_) => StackDialog(
|
|
title:
|
|
"Failed to update trade estimate",
|
|
message:
|
|
"${response.value!.warningMessage!}\n\nDo you want to attempt trade anyways?",
|
|
leftButton: TextButton(
|
|
style: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.getSecondaryEnabledButtonColor(
|
|
context),
|
|
child: Text(
|
|
"Cancel",
|
|
style: STextStyles.itemSubtitle12(
|
|
context),
|
|
),
|
|
onPressed: () {
|
|
// notify return to cancel
|
|
Navigator.of(context).pop(true);
|
|
},
|
|
),
|
|
rightButton: TextButton(
|
|
style: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.getPrimaryEnabledButtonColor(
|
|
context),
|
|
child: Text(
|
|
"Attempt",
|
|
style: STextStyles.button(context),
|
|
),
|
|
onPressed: () {
|
|
// continue and try to attempt trade
|
|
Navigator.of(context).pop(false);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (shouldCancel is bool && shouldCancel) {
|
|
return;
|
|
}
|
|
|
|
String rate =
|
|
"1 ${fromTicker.toUpperCase()} ~${ref.read(fixedRateExchangeFormProvider).rate!.toStringAsFixed(8)} ${toTicker.toUpperCase()}";
|
|
|
|
final model = IncompleteExchangeModel(
|
|
sendTicker: fromTicker,
|
|
receiveTicker: toTicker,
|
|
rateInfo: rate,
|
|
sendAmount: sendAmount,
|
|
receiveAmount: response.value!.toAmount,
|
|
rateId: response.value!.rateId,
|
|
rateType: rateType,
|
|
);
|
|
|
|
if (mounted) {
|
|
ref
|
|
.read(
|
|
exchangeSendFromWalletIdStateProvider
|
|
.state)
|
|
.state = null;
|
|
unawaited(Navigator.of(context).pushNamed(
|
|
Step1View.routeName,
|
|
arguments: model,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
: null,
|
|
child: Text(
|
|
"Exchange",
|
|
style: STextStyles.button(context),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 20,
|
|
),
|
|
// Text(
|
|
// "Trades",
|
|
// style: STextStyles.itemSubtitle(context).copyWith(
|
|
// color: Theme.of(context).extension<StackColors>()!.textDark3,
|
|
// ),
|
|
// ),
|
|
// SizedBox(
|
|
// height: 12,
|
|
// ),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
)
|
|
];
|
|
},
|
|
body: Builder(
|
|
builder: (buildContext) {
|
|
final trades = ref
|
|
.watch(tradesServiceProvider.select((value) => value.trades));
|
|
final tradeCount = trades.length;
|
|
final hasHistory = tradeCount > 0;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
child: CustomScrollView(
|
|
slivers: [
|
|
SliverOverlapInjector(
|
|
handle: NestedScrollView.sliverOverlapAbsorberHandleFor(
|
|
buildContext),
|
|
),
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
const SizedBox(
|
|
height: 12,
|
|
),
|
|
Text(
|
|
"Trades",
|
|
style: STextStyles.itemSubtitle(context).copyWith(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.textDark3,
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 12,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (hasHistory)
|
|
SliverList(
|
|
delegate: SliverChildBuilderDelegate((context, index) {
|
|
return Padding(
|
|
padding: const EdgeInsets.all(4),
|
|
child: TradeCard(
|
|
key: Key("tradeCard_${trades[index].uuid}"),
|
|
trade: trades[index],
|
|
onTap: () async {
|
|
final String tradeId = trades[index].id;
|
|
|
|
final lookup = ref
|
|
.read(tradeSentFromStackLookupProvider)
|
|
.all;
|
|
|
|
debugPrint("ALL: $lookup");
|
|
|
|
final String? txid = ref
|
|
.read(tradeSentFromStackLookupProvider)
|
|
.getTxidForTradeId(tradeId);
|
|
final List<String>? walletIds = ref
|
|
.read(tradeSentFromStackLookupProvider)
|
|
.getWalletIdsForTradeId(tradeId);
|
|
|
|
if (txid != null &&
|
|
walletIds != null &&
|
|
walletIds.isNotEmpty) {
|
|
final manager = ref
|
|
.read(walletsChangeNotifierProvider)
|
|
.getManager(walletIds.first);
|
|
|
|
debugPrint("name: ${manager.walletName}");
|
|
|
|
// TODO store tx data completely locally in isar so we don't lock up ui here when querying txData
|
|
final txData = await manager.transactionData;
|
|
|
|
final tx = txData.getAllTransactions()[txid];
|
|
|
|
if (mounted) {
|
|
unawaited(Navigator.of(context).pushNamed(
|
|
TradeDetailsView.routeName,
|
|
arguments: Tuple4(tradeId, tx,
|
|
walletIds.first, manager.walletName),
|
|
));
|
|
}
|
|
} else {
|
|
unawaited(Navigator.of(context).pushNamed(
|
|
TradeDetailsView.routeName,
|
|
arguments: Tuple4(
|
|
tradeId, null, walletIds?.first, null),
|
|
));
|
|
}
|
|
},
|
|
),
|
|
);
|
|
}, childCount: tradeCount),
|
|
),
|
|
if (!hasHistory)
|
|
SliverToBoxAdapter(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.popupBG,
|
|
borderRadius: BorderRadius.circular(
|
|
Constants.size.circularBorderRadius,
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Text(
|
|
"Trades will appear here",
|
|
textAlign: TextAlign.center,
|
|
style: STextStyles.itemSubtitle(context),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class RateInfo extends ConsumerWidget {
|
|
const RateInfo({
|
|
Key? key,
|
|
required this.onChanged,
|
|
}) : super(key: key);
|
|
|
|
final void Function(ExchangeRateType) onChanged;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final type = ref.watch(
|
|
prefsChangeNotifierProvider.select((pref) => pref.exchangeRateType));
|
|
final isEstimated = type == ExchangeRateType.estimated;
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).extension<StackColors>()!.popupBG,
|
|
borderRadius: BorderRadius.circular(
|
|
Constants.size.circularBorderRadius,
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
TextButton(
|
|
onPressed: kFixedRateEnabled
|
|
? () async {
|
|
if (isEstimated) {
|
|
if (ref
|
|
.read(
|
|
changeNowFixedInitialLoadStatusStateProvider
|
|
.state)
|
|
.state ==
|
|
ChangeNowLoadStatus.loading) {
|
|
bool userPoppedDialog = false;
|
|
await showDialog<void>(
|
|
context: context,
|
|
builder: (context) => Consumer(
|
|
builder: (context, ref, __) {
|
|
return StackOkDialog(
|
|
title: "Loading rate data...",
|
|
message:
|
|
"Performing initial fetch of ChangeNOW fixed rate market data",
|
|
onOkPressed: (value) {
|
|
userPoppedDialog = value == "OK";
|
|
},
|
|
);
|
|
},
|
|
),
|
|
);
|
|
if (ref
|
|
.read(
|
|
changeNowFixedInitialLoadStatusStateProvider
|
|
.state)
|
|
.state ==
|
|
ChangeNowLoadStatus.loading) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
unawaited(showModalBottomSheet<dynamic>(
|
|
backgroundColor: Colors.transparent,
|
|
context: context,
|
|
shape: const RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(20),
|
|
),
|
|
),
|
|
builder: (_) => const ExchangeRateSheet(),
|
|
).then((value) {
|
|
if (value is ExchangeRateType && value != type) {
|
|
onChanged(value);
|
|
}
|
|
}));
|
|
}
|
|
: null,
|
|
style: Theme.of(context).textButtonTheme.style?.copyWith(
|
|
minimumSize: MaterialStateProperty.all(
|
|
const Size(0, 0),
|
|
),
|
|
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
|
const EdgeInsets.all(2),
|
|
),
|
|
backgroundColor: MaterialStateProperty.all<Color>(
|
|
Colors.transparent,
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
isEstimated ? "Estimated rate" : "Fixed rate",
|
|
style: STextStyles.itemSubtitle(context),
|
|
),
|
|
const SizedBox(
|
|
width: 6,
|
|
),
|
|
if (kFixedRateEnabled)
|
|
SvgPicture.asset(
|
|
Assets.svg.chevronDown,
|
|
width: 5,
|
|
height: 2.5,
|
|
color: Theme.of(context)
|
|
.extension<StackColors>()!
|
|
.infoItemLabel,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(
|
|
minWidth: 1,
|
|
),
|
|
child: Text(
|
|
isEstimated
|
|
? ref.watch(estimatedRateExchangeFormProvider
|
|
.select((value) => value.rateDisplayString))
|
|
: ref.watch(fixedRateExchangeFormProvider
|
|
.select((value) => value.rateDisplayString)),
|
|
style: STextStyles.itemSubtitle12(context),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|