From 290adfec2192a4a7e7362870daebda5f86c24708 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 10 Jan 2023 15:25:20 -0600 Subject: [PATCH] stub pages for mobile and desktop lots of extra code, lots of commented sections, the models are wrong, the pages just load on desktop and mobile. need to complete the form and ... well, there's a lot, really --- lib/models/buy/buy_form_state.dart | 428 ++++++++++++++++++ lib/models/buy/response_objects/buy.dart | 237 ++++++++++ lib/models/buy/response_objects/currency.dart | 123 +++++ lib/pages/buy_view/buy_form.dart | 227 ++++++++++ lib/pages/buy_view/buy_view.dart | 176 +++++-- lib/pages/home_view/home_view.dart | 2 +- .../sub_widgets/home_view_button_bar.dart | 2 +- .../sub_widgets/wallet_navigation_bar.dart | 62 +-- .../buy/buy_form_state_provider.dart | 6 + .../global/buys_service_provider.dart | 5 + lib/services/buy/buy.dart | 44 ++ lib/services/buys_service.dart | 66 +++ 12 files changed, 1318 insertions(+), 60 deletions(-) create mode 100644 lib/models/buy/buy_form_state.dart create mode 100644 lib/models/buy/response_objects/buy.dart create mode 100644 lib/models/buy/response_objects/currency.dart create mode 100644 lib/pages/buy_view/buy_form.dart create mode 100644 lib/providers/buy/buy_form_state_provider.dart create mode 100644 lib/providers/global/buys_service_provider.dart create mode 100644 lib/services/buy/buy.dart create mode 100644 lib/services/buys_service.dart diff --git a/lib/models/buy/buy_form_state.dart b/lib/models/buy/buy_form_state.dart new file mode 100644 index 000000000..6666e4296 --- /dev/null +++ b/lib/models/buy/buy_form_state.dart @@ -0,0 +1,428 @@ +import 'package:decimal/decimal.dart'; +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; + _onBuyTypeChanged(); + } + + // BuyRateType _buyType = BuyRateType.estimated; + // BuyRateType get buyType => _buyType; + // set buyType(BuyRateType value) { + // _buyType = value; + // _onBuyRateTypeChanged(); + // } + + bool reversed = false; + + Decimal? fromAmount; + Decimal? toAmount; + + Decimal? minAmount; + Decimal? maxAmount; + + Decimal? rate; + // Estimate? estimate; + // + // dynamic? _market; + // dynamic? get market => _market; + // + // dynamic? _from; + // dynamic? _to; + + @override + String toString() { + return 'BuyFormState: {test: "test"}'; + // return 'BuyFormState: {_buy: $_buy, _buyType: $_buyType, reversed: $reversed, fromAmount: $fromAmount, toAmount: $toAmount, minAmount: $minAmount, maxAmount: $maxAmount, rate: $rate, estimate: $estimate, _market: $_market, _from: $_from, _to: $_to, _onError: $_onError}'; + } + + String? get fromTicker { + // switch (buyType) { + // case BuyRateType.estimated: + // return _from?.ticker; + // case BuyRateType.fixed: + // switch (buy?.name) { + // // case SimpleSwapBuy.buyName: + // // return _from?.ticker; + // case ChangeNowBuy.buyName: + // return market?.from; + // default: + // return null; + // } + // } + } + + String? get toTicker { + // switch (buyType) { + // case BuyRateType.estimated: + // return _to?.ticker; + // case BuyRateType.fixed: + // switch (buy?.name) { + // // case SimpleSwapBuy.buyName: + // // return _to?.ticker; + // case ChangeNowBuy.buyName: + // return market?.to; + // default: + // return null; + // } + // } + } + + void Function(String)? _onError; + + // dynamic? get from => _from; + // dynamic? get to => _to; + // + // void setCurrencies(dynamic from, dynamic to) { + // _from = from; + // _to = to; + // } + + String get warning { + // if (reversed) { + // if (toTicker != null && toAmount != null) { + // if (minAmount != null && + // toAmount! < minAmount! && + // toAmount! > Decimal.zero) { + // return "Minimum amount ${minAmount!.toString()} ${toTicker!.toUpperCase()}"; + // } else if (maxAmount != null && toAmount! > maxAmount!) { + // return "Maximum amount ${maxAmount!.toString()} ${toTicker!.toUpperCase()}"; + // } + // } + // } else { + // if (fromTicker != null && fromAmount != null) { + // if (minAmount != null && + // fromAmount! < minAmount! && + // fromAmount! > Decimal.zero) { + // return "Minimum amount ${minAmount!.toString()} ${fromTicker!.toUpperCase()}"; + // } else if (maxAmount != null && fromAmount! > maxAmount!) { + // return "Maximum amount ${maxAmount!.toString()} ${fromTicker!.toUpperCase()}"; + // } + // } + // } + + return ""; + } + + String get fromAmountString => fromAmount?.toStringAsFixed(8) ?? ""; + String get toAmountString => toAmount?.toStringAsFixed(8) ?? ""; + + bool get canBuy { + // if (buy?.name == ChangeNowBuy.buyName && buyType == BuyRateType.fixed) { + // return _market != null && + // fromAmount != null && + // toAmount != null && + // warning.isEmpty; + // } else { + // return fromAmount != null && + // fromAmount != Decimal.zero && + // toAmount != null && + // rate != null && + // warning.isEmpty; + // } + return true; + } + + void clearAmounts(bool shouldNotifyListeners) { + // fromAmount = null; + // toAmount = null; + // minAmount = null; + // maxAmount = null; + // rate = null; + // + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + } + + Future setFromAmountAndCalculateToAmount( + Decimal newFromAmount, + bool shouldNotifyListeners, + ) async { + // if (newFromAmount == Decimal.zero) { + // toAmount = Decimal.zero; + // } + // + // fromAmount = newFromAmount; + // reversed = false; + // + // await updateRanges(shouldNotifyListeners: false); + // + // await updateEstimate( + // shouldNotifyListeners: false, + // reversed: reversed, + // ); + // + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + } + + Future setToAmountAndCalculateFromAmount( + Decimal newToAmount, + bool shouldNotifyListeners, + ) async { + // if (newToAmount == Decimal.zero) { + // fromAmount = Decimal.zero; + // } + // + // toAmount = newToAmount; + // reversed = true; + // + // await updateRanges(shouldNotifyListeners: false); + // + // await updateEstimate( + // shouldNotifyListeners: false, + // reversed: reversed, + // ); + // + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + } + + // Future updateTo(dynamic to, bool shouldNotifyListeners) async { + Future updateTo(dynamic to, bool shouldNotifyListeners) async { + // try { + // _to = to; + // if (_from == null) { + // rate = null; + // notifyListeners(); + // return; + // } + // + // await updateRanges(shouldNotifyListeners: false); + // + // await updateEstimate( + // shouldNotifyListeners: false, + // reversed: reversed, + // ); + // + // //todo: check if print needed + // // debugPrint( + // // "_updated TO: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$fromAmount _toAmount=$toAmount rate:$rate for: $buy"); + // + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + // } catch (e, s) { + // Logging.instance.log("$e\n$s", level: LogLevel.Error); + // } + } + + // Future updateFrom(dynamic from, bool shouldNotifyListeners) async { + Future updateFrom(dynamic from, bool shouldNotifyListeners) async { + // try { + // _from = from; + // + // if (_to == null) { + // rate = null; + // notifyListeners(); + // return; + // } + // + // await updateRanges(shouldNotifyListeners: false); + // + // await updateEstimate( + // shouldNotifyListeners: false, + // reversed: reversed, + // ); + // + // //todo: check if print needed + // // debugPrint( + // // "_updated FROM: _from=${_from!.ticker} _to=${_to!.ticker} _fromAmount=$fromAmount _toAmount=$toAmount rate:$rate for: $buy"); + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + // } catch (e, s) { + // Logging.instance.log("$e\n$s", level: LogLevel.Error); + // } + } + + Future updateMarket( + // dynamic? market, + dynamic? market, + bool shouldNotifyListeners, + ) async { + // _market = market; + // + // if (_market == null) { + // fromAmount = null; + // toAmount = null; + // } else { + // if (fromAmount != null) { + // if (fromAmount! <= Decimal.zero) { + // toAmount = Decimal.zero; + // } else { + // await updateRanges(shouldNotifyListeners: false); + // await updateEstimate( + // shouldNotifyListeners: false, + // reversed: reversed, + // ); + // } + // } + // } + // + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + } + + void _onBuyRateTypeChanged() { + // print("_onBuyRateTypeChanged"); + // updateRanges(shouldNotifyListeners: true).then( + // (_) => updateEstimate( + // shouldNotifyListeners: true, + // reversed: reversed, + // ), + // ); + } + + void _onBuyTypeChanged() { + // updateRanges(shouldNotifyListeners: true).then( + // (_) => updateEstimate( + // shouldNotifyListeners: true, + // reversed: reversed, + // ), + // ); + } + + Future updateRanges({required bool shouldNotifyListeners}) async { + // // if (buy?.name == SimpleSwapBuy.buyName) { + // // reversed = false; + // // } + // final _fromTicker = reversed ? toTicker : fromTicker; + // final _toTicker = reversed ? fromTicker : toTicker; + // if (_fromTicker == null || _toTicker == null) { + // Logging.instance.log( + // "Tried to $runtimeType.updateRanges where (from: $_fromTicker || to: $_toTicker) for: $buy", + // level: LogLevel.Info, + // ); + // return; + // } + // final response = await buy?.getRange( + // _fromTicker, + // _toTicker, + // buyType == BuyRateType.fixed, + // ); + // + // if (response?.value == null) { + // Logging.instance.log( + // "Tried to $runtimeType.updateRanges for: $buy where response: $response", + // level: LogLevel.Info, + // ); + // return; + // } + // + // final range = response!.value!; + // + // minAmount = range.min; + // maxAmount = range.max; + // + // //todo: check if print needed + // // debugPrint( + // // "updated range for: $buy for $_fromTicker-$_toTicker: $range"); + // + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + } + + Future updateEstimate({ + required bool shouldNotifyListeners, + required bool reversed, + }) async { + // // if (buy?.name == SimpleSwapBuy.buyName) { + // // reversed = false; + // // } + // final amount = reversed ? toAmount : fromAmount; + // if (fromTicker == null || + // toTicker == null || + // amount == null || + // amount <= Decimal.zero) { + // Logging.instance.log( + // "Tried to $runtimeType.updateEstimate for: $buy where (from: $fromTicker || to: $toTicker || amount: $amount)", + // level: LogLevel.Info, + // ); + // return; + // } + // final response = await buy?.getEstimate( + // fromTicker!, + // toTicker!, + // amount, + // buyType == BuyRateType.fixed, + // reversed, + // ); + // + // if (response?.value == null) { + // Logging.instance.log( + // "Tried to $runtimeType.updateEstimate for: $buy where response: $response", + // level: LogLevel.Info, + // ); + // return; + // } + // + // estimate = response!.value!; + // + // if (reversed) { + // fromAmount = estimate!.estimatedAmount; + // } else { + // toAmount = estimate!.estimatedAmount; + // } + // + // rate = (toAmount! / fromAmount!).toDecimal(scaleOnInfinitePrecision: 12); + // + // //todo: check if print needed + // // debugPrint( + // // "updated estimate for: $buy for $fromTicker-$toTicker: $estimate"); + // + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + } + + void setOnError({ + required void Function(String)? onError, + bool shouldNotifyListeners = false, + }) { + // _onError = onError; + // if (shouldNotifyListeners) { + // notifyListeners(); + // } + } + + Future swap({dynamic? market}) async { + // final Decimal? newToAmount = fromAmount; + // final Decimal? newFromAmount = toAmount; + // + // fromAmount = newFromAmount; + // toAmount = newToAmount; + // + // minAmount = null; + // maxAmount = null; + // + // if (buyType == BuyRateType.fixed && buy?.name == ChangeNowBuy.buyName) { + // await updateMarket(market, false); + // } else { + // final dynamic? newTo = from; + // final dynamic? newFrom = to; + // + // _to = newTo; + // _from = newFrom; + // + // await updateRanges(shouldNotifyListeners: false); + // + // await updateEstimate( + // shouldNotifyListeners: false, + // reversed: reversed, + // ); + // } + // + // notifyListeners(); + } +} diff --git a/lib/models/buy/response_objects/buy.dart b/lib/models/buy/response_objects/buy.dart new file mode 100644 index 000000000..4317faf99 --- /dev/null +++ b/lib/models/buy/response_objects/buy.dart @@ -0,0 +1,237 @@ +import 'package:hive/hive.dart'; + +part 'buy.g.dart'; + +@HiveType(typeId: Buy.typeId) +class Buy { + static const typeId = 22; + + @HiveField(0) + final String uuid; + + @HiveField(1) + final String tradeId; + + @HiveField(2) + final String rateType; + + @HiveField(3) + final String direction; + + @HiveField(4) + final DateTime timestamp; + + @HiveField(5) + final DateTime updatedAt; + + @HiveField(6) + final String payInCurrency; + + @HiveField(7) + final String payInAmount; + + @HiveField(8) + final String payInAddress; + + @HiveField(9) + final String payInNetwork; + + @HiveField(10) + final String payInExtraId; + + @HiveField(11) + final String payInTxid; + + @HiveField(12) + final String payOutCurrency; + + @HiveField(13) + final String payOutAmount; + + @HiveField(14) + final String payOutAddress; + + @HiveField(15) + final String payOutNetwork; + + @HiveField(16) + final String payOutExtraId; + + @HiveField(17) + final String payOutTxid; + + @HiveField(18) + final String refundAddress; + + @HiveField(19) + final String refundExtraId; + + @HiveField(20) + final String status; + + @HiveField(21) + final String exchangeName; + + const Buy({ + required this.uuid, + required this.tradeId, + required this.rateType, + required this.direction, + required this.timestamp, + required this.updatedAt, + required this.payInCurrency, + required this.payInAmount, + required this.payInAddress, + required this.payInNetwork, + required this.payInExtraId, + required this.payInTxid, + required this.payOutCurrency, + required this.payOutAmount, + required this.payOutAddress, + required this.payOutNetwork, + required this.payOutExtraId, + required this.payOutTxid, + required this.refundAddress, + required this.refundExtraId, + required this.status, + required this.exchangeName, + }); + + Trade copyWith({ + String? tradeId, + String? rateType, + String? direction, + DateTime? timestamp, + DateTime? updatedAt, + String? payInCurrency, + String? payInAmount, + String? payInAddress, + String? payInNetwork, + String? payInExtraId, + String? payInTxid, + String? payOutCurrency, + String? payOutAmount, + String? payOutAddress, + String? payOutNetwork, + String? payOutExtraId, + String? payOutTxid, + String? refundAddress, + String? refundExtraId, + String? status, + String? exchangeName, + }) { + return Buy( + uuid: uuid, + tradeId: tradeId ?? this.tradeId, + rateType: rateType ?? this.rateType, + direction: direction ?? this.direction, + timestamp: timestamp ?? this.timestamp, + updatedAt: updatedAt ?? this.updatedAt, + payInCurrency: payInCurrency ?? this.payInCurrency, + payInAmount: payInAmount ?? this.payInAmount, + payInAddress: payInAddress ?? this.payInAddress, + payInNetwork: payInNetwork ?? this.payInNetwork, + payInExtraId: payInExtraId ?? this.payInExtraId, + payInTxid: payInTxid ?? this.payInTxid, + payOutCurrency: payOutCurrency ?? this.payOutCurrency, + payOutAmount: payOutAmount ?? this.payOutAmount, + payOutAddress: payOutAddress ?? this.payOutAddress, + payOutNetwork: payOutNetwork ?? this.payOutNetwork, + payOutExtraId: payOutExtraId ?? this.payOutExtraId, + payOutTxid: payOutTxid ?? this.payOutTxid, + refundAddress: refundAddress ?? this.refundAddress, + refundExtraId: refundExtraId ?? this.refundExtraId, + status: status ?? this.status, + exchangeName: exchangeName ?? this.exchangeName, + ); + } + + Map toMap() { + return { + "uuid": uuid, + "tradeId": tradeId, + "rateType": rateType, + "direction": direction, + "timestamp": timestamp.toIso8601String(), + "updatedAt": updatedAt.toIso8601String(), + "payInCurrency": payInCurrency, + "payInAmount": payInAmount, + "payInAddress": payInAddress, + "payInNetwork": payInNetwork, + "payInExtraId": payInExtraId, + "payInTxid": payInTxid, + "payOutCurrency": payOutCurrency, + "payOutAmount": payOutAmount, + "payOutAddress": payOutAddress, + "payOutNetwork": payOutNetwork, + "payOutExtraId": payOutExtraId, + "payOutTxid": payOutTxid, + "refundAddress": refundAddress, + "refundExtraId": refundExtraId, + "status": status, + "exchangeName": exchangeName, + }; + } + + factory Buy.fromMap(Map map) { + return Buy( + uuid: map["uuid"] as String, + tradeId: map["tradeId"] as String, + rateType: map["rateType"] as String, + direction: map["direction"] as String, + timestamp: DateTime.parse(map["timestamp"] as String), + updatedAt: DateTime.parse(map["updatedAt"] as String), + payInCurrency: map["payInCurrency"] as String, + payInAmount: map["payInAmount"] as String, + payInAddress: map["payInAddress"] as String, + payInNetwork: map["payInNetwork"] as String, + payInExtraId: map["payInExtraId"] as String, + payInTxid: map["payInTxid"] as String, + payOutCurrency: map["payOutCurrency"] as String, + payOutAmount: map["payOutAmount"] as String, + payOutAddress: map["payOutAddress"] as String, + payOutNetwork: map["payOutNetwork"] as String, + payOutExtraId: map["payOutExtraId"] as String, + payOutTxid: map["payOutTxid"] as String, + refundAddress: map["refundAddress"] as String, + refundExtraId: map["refundExtraId"] as String, + status: map["status"] as String, + exchangeName: map["exchangeName"] as String, + ); + } + + // factory Trade.fromExchangeTransaction( + // ExchangeTransaction exTx, bool reversed) { + // return Buy( + // uuid: exTx.uuid, + // tradeId: exTx.id, + // rateType: "", + // direction: reversed ? "reverse" : "direct", + // timestamp: exTx.date, + // updatedAt: DateTime.tryParse(exTx.statusObject!.updatedAt) ?? exTx.date, + // payInCurrency: exTx.fromCurrency, + // payInAmount: exTx.statusObject!.amountSendDecimal.isEmpty + // ? exTx.statusObject!.expectedSendAmountDecimal + // : exTx.statusObject!.amountSendDecimal, + // payInAddress: exTx.payinAddress, + // payInNetwork: "", + // payInExtraId: exTx.payinExtraId, + // payInTxid: exTx.statusObject!.payinHash, + // payOutCurrency: exTx.toCurrency, + // payOutAmount: exTx.amount, + // payOutAddress: exTx.payoutAddress, + // payOutNetwork: "", + // payOutExtraId: exTx.payoutExtraId, + // payOutTxid: exTx.statusObject!.payoutHash, + // refundAddress: exTx.refundAddress, + // refundExtraId: exTx.refundExtraId, + // status: exTx.statusObject!.status.name, + // exchangeName: ChangeNowExchange.exchangeName, + // ); + // } + + @override + String toString() { + return toMap().toString(); + } +} diff --git a/lib/models/buy/response_objects/currency.dart b/lib/models/buy/response_objects/currency.dart new file mode 100644 index 000000000..0850f9a38 --- /dev/null +++ b/lib/models/buy/response_objects/currency.dart @@ -0,0 +1,123 @@ +class Currency { + /// Currency ticker + final String ticker; + + /// Currency name + final String name; + + /// Currency network + final String network; + + /// Currency 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; + + Currency({ + 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 Currency.fromJson(Map json) { + try { + return Currency( + 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; + } + + Currency copyWith({ + String? ticker, + String? name, + String? network, + String? image, + bool? hasExternalId, + String? externalId, + bool? isFiat, + bool? featured, + bool? isStable, + bool? supportsFixedRate, + bool? isAvailable, + }) { + return Currency( + 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 "Currency: ${toJson()}"; + } +} diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart new file mode 100644 index 000000000..31515815a --- /dev/null +++ b/lib/pages/buy_view/buy_form.dart @@ -0,0 +1,227 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/providers/buy/buy_form_state_provider.dart'; +import 'package:stackwallet/utilities/assets.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/conditional_parent.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; + +class BuyForm extends ConsumerStatefulWidget { + const BuyForm({ + Key? key, + this.walletId, + this.coin, + }) : super(key: key); + + final String? walletId; + final Coin? coin; + + @override + ConsumerState createState() => _BuyFormState(); +} + +class _BuyFormState extends ConsumerState { + late final String? walletId; + late final Coin? coin; + + final isDesktop = Util.isDesktop; + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "You will send", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + SizedBox( + height: isDesktop ? 10 : 4, + ), + // ExchangeTextField( + // controller: _sendController, + // focusNode: _sendFocusNode, + // 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 (_sendController.text == "-") { + // _sendController.text = ""; + // } + // }, + // onChanged: sendFieldOnChanged, + // onButtonTap: selectSendCurrency, + // isWalletCoin: isWalletCoin(coin, true), + // image: _fetchIconUrlFromTicker(ref.watch( + // buyFormStateProvider.select((value) => value.fromTicker))), + // ticker: ref.watch( + // buyFormStateProvider.select((value) => value.fromTicker)), + // ), + SizedBox( + height: isDesktop ? 10 : 4, + ), + SizedBox( + height: isDesktop ? 10 : 4, + ), + // if (ref + // .watch(buyFormStateProvider.select((value) => value.warning)) + // .isNotEmpty && + // !ref.watch(buyFormStateProvider.select((value) => value.reversed))) + // Text( + // ref.watch(buyFormStateProvider.select((value) => value.warning)), + // style: STextStyles.errorSmall(context), + // ), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "You will receive", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ), + child: RoundedContainer( + padding: isDesktop + ? const EdgeInsets.all(6) + : const EdgeInsets.all(2), + color: Theme.of(context) + .extension()! + .buttonBackSecondary, + radiusMultiplier: 0.75, + child: 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()! + .accentColorDark, + ), + ), + ), + ), + ), + ], + ), + SizedBox( + height: isDesktop ? 10 : 7, + ), + // ExchangeTextField( + // focusNode: _receiveFocusNode, + // controller: _receiveController, + // 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 (!(ref.read(prefsChangeNotifierProvider).exchangeRateType == + // ExchangeRateType.estimated) && + // _receiveController.text == "-") { + // _receiveController.text = ""; + // } + // }, + // onChanged: receiveFieldOnChanged, + // onButtonTap: selectReceiveCurrency, + // isWalletCoin: isWalletCoin(coin, true), + // image: _fetchIconUrlFromTicker(ref.watch( + // buyFormStateProvider.select((value) => value.toTicker))), + // ticker: ref.watch( + // buyFormStateProvider.select((value) => value.toTicker)), + // readOnly: ref.watch(prefsChangeNotifierProvider + // .select((value) => value.exchangeRateType)) == + // ExchangeRateType.estimated, + // // || + // // ref.watch(exchangeProvider).name == + // // SimpleSwapExchange.exchangeName, + // ), + // if (ref + // .watch(buyFormStateProvider.select((value) => value.warning)) + // .isNotEmpty && + // ref.watch(buyFormStateProvider.select((value) => value.reversed))) + // Text( + // ref.watch(buyFormStateProvider.select((value) => value.warning)), + // style: STextStyles.errorSmall(context), + // ), + SizedBox( + height: isDesktop ? 20 : 12, + ), + // SizedBox( + // height: 60, + // child: RateTypeToggle( + // onChanged: onRateTypeChanged, + // ), + // ), + // these reads should be watch + if (ref.watch(buyFormStateProvider).fromAmount != null && + ref.watch(buyFormStateProvider).fromAmount != Decimal.zero) + SizedBox( + height: isDesktop ? 20 : 12, + ), + // these reads should be watch + // if (ref.watch(buyFormStateProvider).fromAmount != null && + // ref.watch(buyFormStateProvider).fromAmount != Decimal.zero) + // ExchangeProviderOptions( + // from: ref.watch(buyFormStateProvider).fromTicker, + // to: ref.watch(buyFormStateProvider).toTicker, + // fromAmount: ref.watch(buyFormStateProvider).fromAmount, + // toAmount: ref.watch(buyFormStateProvider).toAmount, + // fixedRate: ref.watch(prefsChangeNotifierProvider + // .select((value) => value.exchangeRateType)) == + // ExchangeRateType.fixed, + // reversed: ref + // .watch(buyFormStateProvider.select((value) => value.reversed)), + // ), + SizedBox( + height: isDesktop ? 20 : 12, + ), + // PrimaryButton( + // buttonHeight: isDesktop ? ButtonHeight.l : null, + // enabled: ref + // .watch(buyFormStateProvider.select((value) => value.canExchange)), + // // onPressed: ref.watch(buyFormStateProvider + // // .select((value) => value.canExchange)) + // // ? onExchangePressed + // // : null, + // label: "Exchange", + // ) + ], + ); + } +} diff --git a/lib/pages/buy_view/buy_view.dart b/lib/pages/buy_view/buy_view.dart index cc536dd45..32cb38082 100644 --- a/lib/pages/buy_view/buy_view.dart +++ b/lib/pages/buy_view/buy_view.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/buy_view/buy_form.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; class BuyView extends StatefulWidget { const BuyView({Key? key}) : super(key: key); @@ -13,37 +16,156 @@ class _BuyViewState extends State { 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), + child: NestedScrollView( + floatHeaderSlivers: true, + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + ), + child: BuyForm(), ), ), - ], - ), + ) + ]; + }, + body: Builder( + builder: (buildContext) { + // final buys = + // ref.watch(buysServiceProvider.select((value) => value.buys)); + // final buyCount = buys.length; + // final hasHistory = buyCount > 0; + const hasHistory = false; + + 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()! + .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].tradeId; + // + // final lookup = ref + // .read(tradeSentFromStackLookupProvider) + // .all; + // + // //todo: check if print needed + // // debugPrint("ALL: $lookup"); + // + // final String? txid = ref + // .read(tradeSentFromStackLookupProvider) + // .getTxidForTradeId(tradeId); + // final List? walletIds = ref + // .read(tradeSentFromStackLookupProvider) + // .getWalletIdsForTradeId(tradeId); + // + // if (txid != null && + // walletIds != null && + // walletIds.isNotEmpty) { + // final manager = ref + // .read(walletsChangeNotifierProvider) + // .getManager(walletIds.first); + // + // //todo: check if print needed + // // 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()! + .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), + ), + ), + ), + ), + ), + ], + ), + ); + }, ), - // 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"), - // ), - // ], - // ), ), ); } diff --git a/lib/pages/home_view/home_view.dart b/lib/pages/home_view/home_view.dart index 8f048b8b6..ca55bb1a0 100644 --- a/lib/pages/home_view/home_view.dart +++ b/lib/pages/home_view/home_view.dart @@ -308,7 +308,7 @@ class _HomeViewState extends ConsumerState { if (next == 1) { _exchangeDataLoadingService.loadAll(ref); } - if (next >= 0 && next <= 1) { + if (next >= 0 && next <= 2) { _pageController.animateToPage( next, duration: const Duration(milliseconds: 300), 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 658f87fe0..4ac1c82cc 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 @@ -153,7 +153,7 @@ class _HomeViewButtonBarState extends ConsumerState { "Buy", style: STextStyles.button(context).copyWith( fontSize: 14, - color: selectedIndex == 1 + color: selectedIndex == 2 ? Theme.of(context) .extension()! .buttonTextPrimary 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 04147c883..27601a361 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -200,37 +200,37 @@ class WalletNavigationBar extends StatelessWidget { ), // 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(), - // ], - // ), - // ), - // ), - // ), + 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.buyDesktop, + width: 24, + height: 24, + ), + SizedBox( + height: 4, + ), + Text( + "Buy2", + style: STextStyles.buttonSmall(context), + ), + Spacer(), + ], + ), + ), + ), + ), ], ), ), 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/global/buys_service_provider.dart b/lib/providers/global/buys_service_provider.dart new file mode 100644 index 000000000..dc84e2057 --- /dev/null +++ b/lib/providers/global/buys_service_provider.dart @@ -0,0 +1,5 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/services/buys_service.dart'; + +final buysServiceProvider = + ChangeNotifierProvider((ref) => BuysService()); diff --git a/lib/services/buy/buy.dart b/lib/services/buy/buy.dart new file mode 100644 index 000000000..cbad13a99 --- /dev/null +++ b/lib/services/buy/buy.dart @@ -0,0 +1,44 @@ +abstract class Buy { + String get name; + + // Future>> getAllCurrencies(bool fixedRate); + // + // Future>> getPairsFor( + // String currency, + // bool fixedRate, + // ); + // + // Future>> getAllPairs(bool fixedRate); + // + // Future> getTrade(String tradeId); + // Future> updateTrade(Trade trade); + // + // Future>> getTrades(); + // + // Future> getRange( + // String from, + // String to, + // bool fixedRate, + // ); + // + // Future> getEstimate( + // String from, + // String to, + // Decimal amount, + // bool fixedRate, + // bool reversed, + // ); + // + // Future> createTrade({ + // required String from, + // required String to, + // required bool fixedRate, + // required Decimal amount, + // required String addressTo, + // String? extraId, + // required String addressRefund, + // required String refundExtraId, + // String? rateId, + // required bool reversed, + // }); +} diff --git a/lib/services/buys_service.dart b/lib/services/buys_service.dart new file mode 100644 index 000000000..f8d4d4fdc --- /dev/null +++ b/lib/services/buys_service.dart @@ -0,0 +1,66 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/models/buy/response_objects/buy.dart'; + +class BuysService extends ChangeNotifier { + List get Buys { + final list = DB.instance.values(boxName: DB.boxNameBuys); + list.sort((a, b) => + b.timestamp.millisecondsSinceEpoch - + a.timestamp.millisecondsSinceEpoch); + return list; + } + + Buy? get(String BuyId) { + try { + return DB.instance + .values(boxName: DB.boxNameBuys) + .firstWhere((e) => e.BuyId == BuyId); + } catch (_) { + return null; + } + } + + Future add({ + required Buy Buy, + required bool shouldNotifyListeners, + }) async { + await DB.instance + .put(boxName: DB.boxNameBuys, key: Buy.uuid, value: Buy); + + if (shouldNotifyListeners) { + notifyListeners(); + } + } + + Future edit({ + required Buy Buy, + required bool shouldNotifyListeners, + }) async { + if (DB.instance.get(boxName: DB.boxNameBuys, key: Buy.uuid) == null) { + throw Exception("Attempted to edit a Buy that does not exist in Hive!"); + } + + // add overwrites so this edit function is just a wrapper with an extra check + await add(Buy: Buy, shouldNotifyListeners: shouldNotifyListeners); + } + + Future delete({ + required Buy Buy, + required bool shouldNotifyListeners, + }) async { + await deleteByUuid( + uuid: Buy.uuid, shouldNotifyListeners: shouldNotifyListeners); + } + + Future deleteByUuid({ + required String uuid, + required bool shouldNotifyListeners, + }) async { + await DB.instance.delete(boxName: DB.boxNameBuys, key: uuid); + + if (shouldNotifyListeners) { + notifyListeners(); + } + } +}