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(); + } + } +}