update the flow

This commit is contained in:
Serhii 2024-10-08 18:10:07 +03:00
parent e745600b30
commit 534bed355c
13 changed files with 309 additions and 194 deletions

View file

@ -117,10 +117,10 @@ packages:
dependency: "direct overridden"
description:
name: build_runner_core
sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292"
sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e"
url: "https://pub.dev"
source: hosted
version: "7.2.7"
version: "7.2.7+1"
built_collection:
dependency: transitive
description:

View file

@ -2,6 +2,7 @@ import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cw_core/currency.dart';
enum ProviderRecommendation { bestRate, lowKyc, successRate }
@ -47,6 +48,7 @@ class Quote extends SelectableOption {
this.rampId,
this.rampName,
this.rampIconPath,
this.limits,
}) : super(title: provider.isAggregator ? rampName ?? '' : provider.title);
final double rate;
@ -64,6 +66,7 @@ class Quote extends SelectableOption {
bool isSelected = false;
bool isBestRate = false;
bool isBuyAction;
Limits? limits;
late Currency sourceCurrency;
late Currency destinationCurrency;
@ -83,18 +86,32 @@ class Quote extends SelectableOption {
List<String> get badges => recommendations.map((e) => e.title).toList();
@override
String get leftSubTitle => this.rate > 0
? '1 ${isBuyAction ? destinationCurrency : sourceCurrency} = ${rate.toStringAsFixed(2)} ${isBuyAction ? sourceCurrency : destinationCurrency}'
: '';
String get topLeftSubTitle =>
this.rate > 0 ? '1 $fiatName = ${rate.toStringAsFixed(2)} $cryptoName' : '';
@override
String? get rightSubTitle => '';
String get bottomLeftSubTitle {
if (limits != null) {
final min = limits!.min;
final max = limits!.max;
return 'min: ${min} ${sourceCurrency.toString()} | max: ${max == double.infinity ? '' : '${max} ${sourceCurrency.toString()}'}';
}
return '';
}
String get cryptoName => isBuyAction ? sourceCurrency.toString() : destinationCurrency.toString();
String get fiatName => isBuyAction ? destinationCurrency.toString() : sourceCurrency.toString();
@override
String get rightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : '';
String? get topRightSubTitle => '';
@override
String get rightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : '';
String get topRightSubTitleLightIconPath => provider.isAggregator ? provider.lightIcon : '';
@override
String get topRightSubTitleDarkIconPath => provider.isAggregator ? provider.darkIcon : '';
String get quoteTitle => '${provider.title} - ${paymentType.name}';
@ -109,8 +126,10 @@ class Quote extends SelectableOption {
void set setDestinationCurrency(Currency destinationCurrency) =>
this.destinationCurrency = destinationCurrency;
factory Quote.fromOnramperJson(Map<String, dynamic> json,
bool isBuyAction, Map<String, dynamic> metaData, PaymentType paymentType) {
void set setLimits(Limits limits) => this.limits = limits;
factory Quote.fromOnramperJson(Map<String, dynamic> json, bool isBuyAction,
Map<String, dynamic> metaData, PaymentType paymentType) {
final rate = _toDouble(json['rate']) ?? 0.0;
final networkFee = _toDouble(json['networkFee']) ?? 0.0;
final transactionFee = _toDouble(json['transactionFee']) ?? 0.0;
@ -129,6 +148,29 @@ class Quote extends SelectableOption {
.whereType<ProviderRecommendation>()
.toList();
final availablePaymentMethods = json['availablePaymentMethods'] as List<dynamic>? ?? [];
double minLimit = 0.0;
double maxLimit = double.infinity;
for (var paymentMethod in availablePaymentMethods) {
if (paymentMethod is Map<String, dynamic>) {
final details = paymentMethod['details'] as Map<String, dynamic>?;
if (details != null) {
final limits = details['limits'] as Map<String, dynamic>?;
if (limits != null && limits.isNotEmpty) {
final firstLimitEntry = limits.values.first as Map<String, dynamic>?;
if (firstLimitEntry != null) {
minLimit = _toDouble(firstLimitEntry['min'])?.roundToDouble() ?? 0.0;
maxLimit = _toDouble(firstLimitEntry['max'])?.roundToDouble() ?? double.infinity;
break;
}
}
}
}
}
return Quote(
rate: rate,
feeAmount: feeAmount,
@ -143,6 +185,7 @@ class Quote extends SelectableOption {
recommendations: enumRecommendations,
provider: ProvidersHelper.getProviderByType(ProviderType.onramper)!,
isBuyAction: isBuyAction,
limits: Limits(min: minLimit, max: maxLimit),
);
}
@ -155,6 +198,17 @@ class Quote extends SelectableOption {
final networkFee = _toDouble(json['networkFeeAmount']) ?? 0.0;
final transactionFee = _toDouble(json['extraFeeAmount']) ?? 0.0;
final feeAmount = double.parse((fee + networkFee + transactionFee).toStringAsFixed(2));
final baseCurrency = json['baseCurrency'] as Map<String, dynamic>?;
double minLimit = 0.0;
double maxLimit = double.infinity;
if (baseCurrency != null) {
minLimit = _toDouble(baseCurrency['minAmount']) ?? minLimit;
maxLimit = _toDouble(baseCurrency['maxAmount']) ?? maxLimit;
}
return Quote(
rate: rate,
feeAmount: feeAmount,
@ -166,23 +220,32 @@ class Quote extends SelectableOption {
quoteId: json['signature'] as String? ?? '',
provider: ProvidersHelper.getProviderByType(ProviderType.moonpay)!,
isBuyAction: isBuyAction,
limits: Limits(min: minLimit, max: maxLimit),
);
}
factory Quote.fromDFXJson(
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
Map<String, dynamic> json,
bool isBuyAction,
PaymentType paymentType,
) {
final rate = _toDouble(json['exchangeRate']) ?? 0.0;
final fees = json['fees'] as Map<String, dynamic>;
final minVolume = _toDouble(json['minVolume']) ?? 0.0;
final maxVolume = _toDouble(json['maxVolume']) ?? double.infinity;
return Quote(
rate: isBuyAction ? rate : 1 / rate,
feeAmount: _toDouble(json['feeAmount']) ?? 0.0,
networkFee: _toDouble(fees['networkFee']) ?? 0.0,
networkFee: _toDouble(fees['network']) ?? 0.0,
transactionFee: _toDouble(fees['rate']) ?? 0.0,
payout: _toDouble(json['payout']) ?? 0.0,
paymentType: paymentType,
recommendations: [ProviderRecommendation.lowKyc],
provider: ProvidersHelper.getProviderByType(ProviderType.dfx)!,
isBuyAction: isBuyAction,
limits: Limits(min: minVolume, max: maxVolume),
);
}
@ -204,11 +267,11 @@ class Quote extends SelectableOption {
recommendations: [],
provider: ProvidersHelper.getProviderByType(ProviderType.robinhood)!,
isBuyAction: isBuyAction,
limits: Limits(min: 0.0, max: double.infinity),
);
}
factory Quote.fromMeldJson(
Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
factory Quote.fromMeldJson(Map<String, dynamic> json, bool isBuyAction, PaymentType paymentType) {
final quotes = json['quotes'][0] as Map<String, dynamic>;
return Quote(
rate: quotes['exchangeRate'] as double? ?? 0.0,
@ -220,6 +283,7 @@ class Quote extends SelectableOption {
recommendations: [],
provider: ProvidersHelper.getProviderByType(ProviderType.meld)!,
isBuyAction: isBuyAction,
limits: Limits(min: 0.0, max: double.infinity),
);
}

View file

@ -264,8 +264,8 @@ class DFXBuyProvider extends BuyProvider {
if (responseData is Map<String, dynamic>) {
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
final quote = Quote.fromDFXJson(responseData, isBuyAction, paymentType);
quote.setSourceCurrency = isBuyAction ? cryptoCurrency : fiatCurrency;
quote.setDestinationCurrency = isBuyAction ? fiatCurrency : cryptoCurrency;
quote.setSourceCurrency = isBuyAction ? fiatCurrency : cryptoCurrency;
quote.setDestinationCurrency = isBuyAction ? cryptoCurrency : fiatCurrency;
return [quote];
} else {
print('DFX: Unexpected data type: ${responseData.runtimeType}');

View file

@ -199,8 +199,8 @@ class MoonPayProvider extends BuyProvider {
final quote =
Quote.fromMoonPayJson(data, isBuyAction, _getPaymentTypeByString(paymentMethods));
quote.setSourceCurrency = isBuyAction ? cryptoCurrency : fiatCurrency;
quote.setDestinationCurrency = isBuyAction ? fiatCurrency : cryptoCurrency;
quote.setSourceCurrency = isBuyAction ? fiatCurrency : cryptoCurrency;
quote.setDestinationCurrency = isBuyAction ? cryptoCurrency : fiatCurrency;
return [quote];
} else {

View file

@ -5,6 +5,7 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/buy/buy_provider.dart';
import 'package:cake_wallet/buy/buy_quote.dart';
import 'package:cake_wallet/buy/payment_method.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
@ -18,8 +19,6 @@ import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
import '../../entities/fiat_currency.dart';
class RobinhoodBuyProvider extends BuyProvider {
RobinhoodBuyProvider(
{required WalletBase wallet, bool isTestEnvironment = false, LedgerViewModel? ledgerVM})
@ -170,8 +169,8 @@ class RobinhoodBuyProvider extends BuyProvider {
if (response.statusCode == 200) {
final paymentType = _getPaymentTypeByString(responseData['paymentMethod'] as String?);
final quote = Quote.fromRobinhoodJson(responseData, isBuyAction, paymentType);
quote.setSourceCurrency = isBuyAction ? cryptoCurrency : fiatCurrency;
quote.setDestinationCurrency = isBuyAction ? fiatCurrency : cryptoCurrency;
quote.setSourceCurrency = isBuyAction ? fiatCurrency : cryptoCurrency;
quote.setDestinationCurrency = isBuyAction ? cryptoCurrency : fiatCurrency;
return [quote];
} else {
if (responseData.containsKey('message')) {

View file

@ -17,15 +17,25 @@ abstract class SelectableOption extends SelectableItem {
String? get description => null;
String? get leftSubTitle => null;
String? get topLeftSubTitle => null;
String? get leftSubTitleIconPath => null;
String? get topLeftSubTitleIconPath => null;
String? get rightSubTitle => null;
String? get topRightSubTitle => null;
String? get rightSubTitleLightIconPath => null;
String? get topRightSubTitleLightIconPath => null;
String? get rightSubTitleDarkIconPath => null;
String? get topRightSubTitleDarkIconPath => null;
String? get bottomLeftSubTitle => null;
String? get bottomLeftSubTitleIconPath => null;
String? get bottomRightSubTitle => null;
String? get bottomRightSubTitleLightIconPath => null;
String? get bottomRightSubTitleDarkIconPath => null;
List<String> get badges => [];

View file

@ -1190,9 +1190,9 @@ Future<void> setup({
getIt.registerFactoryParam<BuyOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {
final items = args.first as List<SelectableItem>;
final pickAnOption = args[1] as void Function(SelectableOption option)?;
final confirmOption = args[2] as void Function(BuildContext contex)?;
return BuyOptionsPage(
items: items, pickAnOption: pickAnOption);
items: items, pickAnOption: pickAnOption, confirmOption: confirmOption);
});
getIt.registerFactoryParam<PaymentMethodOptionsPage, List<dynamic>, void>((List<dynamic> args, _) {

View file

@ -4,10 +4,11 @@ import 'package:cake_wallet/src/screens/select_options_page.dart';
import 'package:flutter/cupertino.dart';
class BuyOptionsPage extends SelectOptionsPage {
BuyOptionsPage({required this.items, this.pickAnOption});
BuyOptionsPage({required this.items, this.pickAnOption, this.confirmOption});
final List<SelectableItem> items;
final Function(SelectableOption option)? pickAnOption;
final Function(BuildContext context)? confirmOption;
@override
String get pageTitle => S.current.choose_a_provider;
@ -27,9 +28,6 @@ class BuyOptionsPage extends SelectOptionsPage {
@override
double? get imageWidth => 40;
@override
TextStyle? get subTitleTextStyle => null;
@override
Color? get selectedBackgroundColor => null;
@ -41,4 +39,10 @@ class BuyOptionsPage extends SelectOptionsPage {
@override
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
@override
String get primaryButtonText => S.current.confirm;
@override
void Function(BuildContext context)? get primaryButtonAction => confirmOption;
}

View file

@ -39,8 +39,6 @@ class BuySellPage extends BasePage {
final _depositAddressFocus = FocusNode();
final _cryptoAmountFocus = FocusNode();
final _receiveAddressFocus = FocusNode();
final _cryptoAmountDebounce = Debounce(Duration(milliseconds: 500));
final _fiatAmountDebounce = Debounce(Duration(milliseconds: 500));
var _isReactionsSet = false;
final arrowBottomPurple = Image.asset(
@ -155,8 +153,6 @@ class BuySellPage extends BasePage {
children: [
SizedBox(height: 12),
_buildPaymentMethodTile(context),
SizedBox(height: 12),
_buildQuoteTile(context)
],
),
),
@ -165,15 +161,15 @@ class BuySellPage extends BasePage {
bottomSection: Column(children: [
Observer(
builder: (_) => LoadingPrimaryButton(
text: 'Next',
text: S.current.choose_a_provider,
onPressed: () async {
if(!_formKey.currentState!.validate()) return;
await buySellViewModel.launchTrade(context);
buySellViewModel.onTapChoseProvider(context);
},
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isDisabled: !buySellViewModel.isReadyToTrade,
isLoading: false)),
isDisabled: false,
isLoading: !buySellViewModel.isReadyToTrade)),
]),
)),
));
@ -214,43 +210,6 @@ class BuySellPage extends BasePage {
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
}
Widget _buildQuoteTile(BuildContext context) {
if (buySellViewModel.buySellQuotState is BuySellQuotLoading ||
buySellViewModel.buySellQuotState is InitialBuySellQuotState) {
return OptionTilePlaceholder(
leadingIcon: Icons.arrow_forward_ios,
borderRadius: 30,
imageWidth: 50,
imageHeight: 50,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
isDarkTheme: buySellViewModel.isDarkTheme);
}
if (buySellViewModel.buySellQuotState is BuySellQuotLoaded &&
buySellViewModel.selectedQuote != null) {
return Observer(builder: (_) {
final selectedQuote = buySellViewModel.selectedQuote!;
return ProviderOptionTile(
lightImagePath: selectedQuote.lightIconPath,
darkImagePath: selectedQuote.darkIconPath,
title: selectedQuote.title,
badges: selectedQuote.badges,
imageWidth: 50,
imageHeight: 50,
leftSubTitle: selectedQuote.leftSubTitle,
rightSubTitleLightIconPath: selectedQuote.rightSubTitleLightIconPath,
rightSubTitleDarkIconPath: selectedQuote.rightSubTitleDarkIconPath,
onPressed: () => _pickQuote(context),
leadingIcon: Icons.arrow_forward_ios,
isLightMode: !buySellViewModel.isDarkTheme,
borderRadius: 30,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
titleTextStyle:
textLargeBold(color: Theme.of(context).extension<CakeTextTheme>()!.titleColor));
});
}
return OptionTilePlaceholder(errorText: 'No quotes available', borderRadius: 30);
}
void _pickPaymentMethod(BuildContext context) async {
final currentOption = buySellViewModel.selectedPaymentMethod;
await Navigator.of(context).pushNamed(
@ -269,16 +228,6 @@ class BuySellPage extends BasePage {
}
}
void _pickQuote(BuildContext context) async {
await Navigator.of(context).pushNamed(
Routes.buyOptionsPage,
arguments: [
buySellViewModel.quoteOptions,
buySellViewModel.changeOption
],
);
}
void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) {
if (_isReactionsSet) {
return;
@ -315,17 +264,13 @@ class BuySellPage extends BasePage {
fiatAmountController.addListener(() {
if (fiatAmountController.text != buySellViewModel.fiatAmount) {
_fiatAmountDebounce.run(() {
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
});
}
});
cryptoAmountController.addListener(() {
if (cryptoAmountController.text != buySellViewModel.cryptoAmount) {
_cryptoAmountDebounce.run(() {
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
});
}
});

View file

@ -27,9 +27,6 @@ class PaymentMethodOptionsPage extends SelectOptionsPage {
@override
double? get imageWidth => null;
@override
TextStyle? get subTitleTextStyle => null;
@override
Color? get selectedBackgroundColor => null;
@ -41,4 +38,10 @@ class PaymentMethodOptionsPage extends SelectOptionsPage {
@override
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
@override
String get primaryButtonText => S.current.confirm;
@override
void Function(BuildContext context)? get primaryButtonAction => null;
}

View file

@ -22,20 +22,22 @@ abstract class SelectOptionsPage extends BasePage {
double? get imageWidth;
TextStyle? get subTitleTextStyle;
Color? get selectedBackgroundColor;
double? get tileBorderRadius;
String get bottomSectionText;
bool get confirmButtonEnabled => true;
bool get primaryButtonEnabled => true;
String get primaryButtonText => '';
List<SelectableItem> get items;
void Function(SelectableOption option)? get onOptionTap;
void Function(BuildContext context)? get primaryButtonAction;
@override
String get title => pageTitle;
@ -44,11 +46,9 @@ abstract class SelectOptionsPage extends BasePage {
return ScrollableWithBottomSection(
content: BodySelectOptionsPage(
items: items,
// Updated to pass items list
onOptionTap: onOptionTap,
tilePadding: tilePadding,
tileBorderRadius: tileBorderRadius,
subTitleTextStyle: subTitleTextStyle,
imageHeight: imageHeight,
imageWidth: imageWidth,
innerPadding: innerPadding),
@ -65,10 +65,14 @@ abstract class SelectOptionsPage extends BasePage {
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
if (confirmButtonEnabled)
if (primaryButtonEnabled)
LoadingPrimaryButton(
text: 'Confirm',
onPressed: () => Navigator.pop(context),
text: primaryButtonText,
onPressed: () {
primaryButtonAction != null
? primaryButtonAction!(context)
: Navigator.pop(context);
},
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isDisabled: false,
@ -86,7 +90,6 @@ class BodySelectOptionsPage extends StatefulWidget {
this.onOptionTap,
this.tilePadding,
this.tileBorderRadius,
this.subTitleTextStyle,
this.imageHeight,
this.imageWidth,
this.innerPadding,
@ -96,7 +99,6 @@ class BodySelectOptionsPage extends StatefulWidget {
final void Function(SelectableOption option)? onOptionTap;
final EdgeInsets? tilePadding;
final double? tileBorderRadius;
final TextStyle? subTitleTextStyle;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? innerPadding;
@ -175,20 +177,20 @@ class _BodySelectOptionsPageState extends State<BodySelectOptionsPage> {
imageWidth: widget.imageWidth,
padding: widget.innerPadding,
description: item.description,
leftSubTitle: item.leftSubTitle,
rightSubTitle: item.rightSubTitle,
rightSubTitleLightIconPath: item.rightSubTitleLightIconPath,
rightSubTitleDarkIconPath: item.rightSubTitleDarkIconPath,
topLeftSubTitle: item.topLeftSubTitle,
topRightSubTitle: item.topRightSubTitle,
rightSubTitleLightIconPath: item.topRightSubTitleLightIconPath,
rightSubTitleDarkIconPath: item.topRightSubTitleDarkIconPath,
bottomLeftSubTitle: item.bottomLeftSubTitle,
badges: item.badges,
isSelected: item.isOptionSelected,
subTitleTextStyle: widget.subTitleTextStyle,
borderRadius: widget.tileBorderRadius,
isLightMode: isLightMode,
onPressed: () => _handleOptionTap(item),
),
);
}
return const SizedBox.shrink(); // Fallback for any unsupported items
return const SizedBox.shrink();
}).toList(),
),
),

View file

@ -10,8 +10,10 @@ class ProviderOptionTile extends StatelessWidget {
required this.lightImagePath,
required this.darkImagePath,
required this.title,
this.leftSubTitle,
this.rightSubTitle,
this.topLeftSubTitle,
this.topRightSubTitle,
this.bottomLeftSubTitle,
this.bottomRightSubTitle,
this.leftSubTitleIconPath,
this.rightSubTitleLightIconPath,
this.rightSubTitleDarkIconPath,
@ -22,7 +24,8 @@ class ProviderOptionTile extends StatelessWidget {
this.imageWidth,
this.padding,
this.titleTextStyle,
this.subTitleTextStyle,
this.firstSubTitleTextStyle,
this.secondSubTitleTextStyle,
this.leadingIcon,
this.selectedBackgroundColor,
this.isSelected = false,
@ -33,8 +36,10 @@ class ProviderOptionTile extends StatelessWidget {
final String lightImagePath;
final String darkImagePath;
final String title;
final String? leftSubTitle;
final String? rightSubTitle;
final String? topLeftSubTitle;
final String? topRightSubTitle;
final String? bottomLeftSubTitle;
final String? bottomRightSubTitle;
final String? leftSubTitleIconPath;
final String? rightSubTitleLightIconPath;
final String? rightSubTitleDarkIconPath;
@ -45,7 +50,8 @@ class ProviderOptionTile extends StatelessWidget {
final double? imageWidth;
final EdgeInsets? padding;
final TextStyle? titleTextStyle;
final TextStyle? subTitleTextStyle;
final TextStyle? firstSubTitleTextStyle;
final TextStyle? secondSubTitleTextStyle;
final IconData? leadingIcon;
final Color? selectedBackgroundColor;
final bool isSelected;
@ -73,7 +79,6 @@ class ProviderOptionTile extends StatelessWidget {
? Theme.of(context).extension<OptionTileTheme>()!.titleColor
: Theme.of(context).cardColor;
final imagePath = isSelected
? isLightMode
? darkImagePath
@ -128,54 +133,18 @@ class ProviderOptionTile extends StatelessWidget {
),
],
),
if (leftSubTitle != null || rightSubTitle != null)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
leftSubTitle != null || leftSubTitleIconPath != null
? Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
if (leftSubTitleIconPath != null &&
leftSubTitleIconPath!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 6),
child: getImage(leftSubTitleIconPath!),
),
Text(
leftSubTitle ?? '',
style: subTitleTextStyle ??
TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textColor),
),
],
),
)
: Offstage(),
rightSubTitle != null || rightSubTitleIconPath != null
? Row(
children: [
if (rightSubTitleIconPath != null && rightSubTitleIconPath.isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 4),
child: getImage(rightSubTitleIconPath, imageColor: textColor),
),
Text(
rightSubTitle ?? '',
style: subTitleTextStyle ??
TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: textColor),
),
],
)
: Offstage(),
],
),
if (topLeftSubTitle != null || topRightSubTitle != null)
subTitleWidget(
leftSubTitle: topLeftSubTitle,
subTitleIconPath: leftSubTitleIconPath,
textColor: textColor,
rightSubTitle: topRightSubTitle,
rightSubTitleIconPath: rightSubTitleIconPath),
if (bottomLeftSubTitle != null || bottomRightSubTitle != null)
subTitleWidget(
leftSubTitle: bottomLeftSubTitle,
textColor: textColor,
subTitleFontSize: 12),
if (badges != null && badges!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 12),
@ -194,6 +163,68 @@ class ProviderOptionTile extends StatelessWidget {
}
}
class subTitleWidget extends StatelessWidget {
const subTitleWidget({
super.key,
this.leftSubTitle,
this.subTitleIconPath,
required this.textColor,
this.rightSubTitle,
this.rightSubTitleIconPath,
this.subTitleFontSize = 16,
});
final String? leftSubTitle;
final String? subTitleIconPath;
final Color textColor;
final String? rightSubTitle;
final String? rightSubTitleIconPath;
final double subTitleFontSize;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
leftSubTitle != null || subTitleIconPath != null
? Row(
children: [
if (subTitleIconPath != null && subTitleIconPath!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 6),
child: getImage(subTitleIconPath!),
),
Text(
leftSubTitle ?? '',
style: TextStyle(
fontSize: subTitleFontSize,
fontWeight: FontWeight.w700,
color: textColor),
),
],
)
: Offstage(),
rightSubTitle != null || rightSubTitleIconPath != null
? Row(
children: [
if (rightSubTitleIconPath != null && rightSubTitleIconPath!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(right: 4),
child: getImage(rightSubTitleIconPath!, imageColor: textColor),
),
Text(
rightSubTitle ?? '',
style: TextStyle(
fontSize: subTitleFontSize, fontWeight: FontWeight.w700, color: textColor),
),
],
)
: Offstage(),
],
);
}
}
class Badge extends StatelessWidget {
Badge({required this.textColor, required this.backgroundColor, required this.title});

View file

@ -10,6 +10,7 @@ import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/provider_types.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/themes/theme_base.dart';
@ -53,12 +54,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
_initialize();
}
@observable
List<CryptoCurrency> cryptoCurrencies;
@observable
List<FiatCurrency> fiatCurrencies;
final NumberFormat _cryptoNumberFormat;
late Timer bestRateSync;
@ -99,12 +94,15 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
SettingsStore settingsStore;
List<SelectableItem> get quoteOptions => [
OptionTitle(title: 'Recommended'),
...sortedRecommendedQuotes,
OptionTitle(title: 'All Providers'),
...sortedQuotes
];
Quote? bestRateQuote;
Quote? selectedQuote;
@observable
List<CryptoCurrency> cryptoCurrencies;
@observable
List<FiatCurrency> fiatCurrencies;
@observable
bool isBuyAction = true;
@ -136,12 +134,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
@observable
String cryptoCurrencyAddress;
@observable
Quote? bestRateQuote;
@observable
Quote? selectedQuote;
@observable
PaymentMethod? selectedPaymentMethod;
@ -152,11 +144,17 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
BuySellQuotLoadingState buySellQuotState;
@computed
bool get isReadyToTrade =>
selectedQuote != null &&
selectedPaymentMethod != null &&
paymentMethodState is PaymentMethodLoaded &&
buySellQuotState is BuySellQuotLoaded;
bool get isReadyToTrade {
final hasSelectedQuote = selectedQuote != null;
final hasSelectedPaymentMethod = selectedPaymentMethod != null;
final isPaymentMethodLoaded = paymentMethodState is PaymentMethodLoaded;
final isBuySellQuotLoaded = buySellQuotState is BuySellQuotLoaded;
return hasSelectedQuote &&
hasSelectedPaymentMethod &&
isPaymentMethodLoaded &&
isBuySellQuotLoaded;
}
@action
void reset() {
@ -209,8 +207,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
.toString()
.replaceAll(RegExp('\\,'), '');
}
await calculateBestRate();
}
@action
@ -235,7 +231,6 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
.toString()
.replaceAll(RegExp('\\,'), '');
}
await calculateBestRate();
}
@action
@ -254,6 +249,61 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
}
}
void onTapChoseProvider(BuildContext context) async {
final initialQuotes = List<Quote>.from(sortedRecommendedQuotes + sortedQuotes);
await calculateBestRate();
final newQuotes = (sortedRecommendedQuotes + sortedQuotes);
for (var quote in newQuotes) quote.limits = null;
final newQuoteProviders = newQuotes
.map((quote) => quote.provider.isAggregator ? quote.rampName : quote.provider.title)
.toSet();
final outOfLimitQuotes = initialQuotes.where((initialQuote) {
return !newQuoteProviders.contains(
initialQuote.provider.isAggregator ? initialQuote.rampName : initialQuote.provider.title);
}).map((missingQuote) {
final quote = Quote(
rate: missingQuote.rate,
feeAmount: missingQuote.feeAmount,
networkFee: missingQuote.networkFee,
transactionFee: missingQuote.transactionFee,
payout: missingQuote.payout,
rampId: missingQuote.rampId,
rampName: missingQuote.rampName,
rampIconPath: missingQuote.rampIconPath,
paymentType: missingQuote.paymentType,
quoteId: missingQuote.quoteId,
recommendations: missingQuote.recommendations,
provider: missingQuote.provider,
isBuyAction: missingQuote.isBuyAction,
limits: missingQuote.limits,
);
quote.sourceCurrency = missingQuote.sourceCurrency;
quote.destinationCurrency = missingQuote.destinationCurrency;
return quote;
}).toList();
final updatedQuoteOptions = List<SelectableItem>.from([
OptionTitle(title: 'Recommended'),
...sortedRecommendedQuotes,
if (sortedQuotes.isNotEmpty) OptionTitle(title: 'All Providers'),
...sortedQuotes,
if (outOfLimitQuotes.isNotEmpty) OptionTitle(title: 'Out of Limits'),
...outOfLimitQuotes,
]);
await Navigator.of(context).pushNamed(
Routes.buyOptionsPage,
arguments: [
updatedQuoteOptions,
changeOption,
launchTrade,
],
).then((value) => calculateBestRate());
}
void _onPairChange() {
_initialize();
}
@ -353,10 +403,17 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
if (sortedRecommendedQuotes.isNotEmpty) {
sortedRecommendedQuotes.first
..isBestRate = true
..isSelected = true
..recommendations.insert(0, ProviderRecommendation.bestRate);
bestRateQuote = sortedRecommendedQuotes.first;
sortedRecommendedQuotes.sort((a, b) {
if (a.provider is OnRamperBuyProvider) return -1;
if (b.provider is OnRamperBuyProvider) return 1;
return 0;
});
selectedQuote = sortedRecommendedQuotes.first;
sortedRecommendedQuotes.first.isSelected = true;
}
buySellQuotState = BuySellQuotLoaded();
@ -365,7 +422,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
@action
Future<void> launchTrade(BuildContext context) async {
final provider = selectedQuote!.provider;
provider.launchProvider(
await provider.launchProvider(
context: context,
quote: selectedQuote!,
amount: amount,