Cw 657 new buy sell flow (#1553)

* init commit

* onramper

* moonPay

* dfx provider

* meld

* dfx payment methods

* fiat buy credentials

* moonpay payment method

* payment loading state

* dfx sell quote

* onramper launch trade

* meld launch trade

* country picker

* update option tile

* buy/sell action

* meld refactor

* update pr_test_build.yml

* ui fixes

* revert country picker commit

* update the ui

* recommended providers

* payment method [skip ci]

* provider option tile

* remove buy action

* minor fixes

* update the best rate when the amount is changed

* fixes for currency title

* fix icons

* code refactoring

* null issue

* code review fixes

* Update pr_test_build_linux.yml

* Update meld_buy_provider.dart

* Update meld_buy_provider.dart

* add show wallets action

* remove default sell / buy provider setting

* localisation

* providerTypes

* icons

* remove duplicate file [skip ci]

* minor changes [skip ci]

* fixes from review

* disable dfx for non eur/chf currencies
fix providers to be fetched with the selected currency

* fix breaking from loop if one element failed

* fix minor naming issue from merging conflicts

* add fiat check for moonpay

* fix address validation

* merge conflict

* fix destination and source currency

* minor fix

* minor fix

* update the flow

* fix bch address format

* fix wallet addresses

* fix initial fetching amount.

* Update address_validator.dart

* review comments

* revert switch case to return null

* minor fix

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Serhii
2024-11-09 21:00:56 +02:00
committed by GitHub
co-authored by OmarHatem
parent cea3084bb3
commit 02f53055b1
88 changed files with 4082 additions and 686 deletions
-74
View File
@@ -1,74 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/option_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
class BuySellOptionsPage extends BasePage {
BuySellOptionsPage(this.dashboardViewModel, this.isBuyAction);
final DashboardViewModel dashboardViewModel;
final bool isBuyAction;
@override
String get title => isBuyAction ? S.current.buy : S.current.sell;
@override
AppBarStyle get appBarStyle => AppBarStyle.regular;
@override
Widget body(BuildContext context) {
final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
final availableProviders = isBuyAction
? dashboardViewModel.availableBuyProviders
: dashboardViewModel.availableSellProviders;
return ScrollableWithBottomSection(
content: Container(
child: Center(
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: 330),
child: Column(
children: [
...availableProviders.map((provider) {
final icon = Image.asset(
isLightMode ? provider.lightIcon : provider.darkIcon,
height: 40,
width: 40,
);
return Padding(
padding: EdgeInsets.only(top: 24),
child: OptionTile(
image: icon,
title: provider.toString(),
description: provider.providerDescription,
onPressed: () => provider.launchProvider(context, isBuyAction),
),
);
}).toList(),
],
),
),
),
),
bottomSection: Padding(
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
child: Text(
isBuyAction
? S.of(context).select_buy_provider_notice
: S.of(context).select_sell_provider_notice,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
),
);
}
}
@@ -0,0 +1,48 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
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, 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;
@override
EdgeInsets? get contentPadding => null;
@override
EdgeInsets? get tilePadding => EdgeInsets.only(top: 8);
@override
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 8);
@override
double? get imageHeight => 40;
@override
double? get imageWidth => 40;
@override
Color? get selectedBackgroundColor => null;
@override
double? get tileBorderRadius => 30;
@override
String get bottomSectionText => '';
@override
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
@override
String get primaryButtonText => S.current.confirm;
@override
void Function(BuildContext context)? get primaryButtonAction => confirmOption;
}
+469
View File
@@ -0,0 +1,469 @@
import 'package:cake_wallet/buy/sell_buy_states.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/src/widgets/trail_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:keyboard_actions/keyboard_actions.dart';
import 'package:mobx/mobx.dart';
class BuySellPage extends BasePage {
BuySellPage(this.buySellViewModel);
final BuySellViewModel buySellViewModel;
final cryptoCurrencyKey = GlobalKey<ExchangeCardState>();
final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
final _formKey = GlobalKey<FormState>();
final _fiatAmountFocus = FocusNode();
final _cryptoAmountFocus = FocusNode();
final _cryptoAddressFocus = FocusNode();
var _isReactionsSet = false;
final arrowBottomPurple = Image.asset(
'assets/images/arrow_bottom_purple_icon.png',
color: Colors.white,
height: 8,
);
final arrowBottomCakeGreen = Image.asset(
'assets/images/arrow_bottom_cake_green.png',
color: Colors.white,
height: 8,
);
late final String? depositWalletName;
late final String? receiveWalletName;
@override
String get title => S.current.buy + '/' + S.current.sell;
@override
bool get gradientBackground => true;
@override
bool get gradientAll => true;
@override
bool get resizeToAvoidBottomInset => false;
@override
bool get extendBodyBehindAppBar => true;
@override
AppBarStyle get appBarStyle => AppBarStyle.transparent;
@override
Function(BuildContext)? get pushToNextWidget => (context) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.focusedChild?.unfocus();
}
};
@override
Widget trailing(BuildContext context) => TrailButton(
caption: S.of(context).clear,
onPressed: () {
_formKey.currentState?.reset();
buySellViewModel.reset();
});
@override
Widget? leading(BuildContext context) {
final _backButton = Icon(
Icons.arrow_back_ios,
color: titleColor(context),
size: 16,
);
final _closeButton =
currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage;
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
return MergeSemantics(
child: SizedBox(
height: isMobileView ? 37 : 45,
width: isMobileView ? 37 : 45,
child: ButtonTheme(
minWidth: double.minPositive,
child: Semantics(
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
child: TextButton(
style: ButtonStyle(
overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent),
),
onPressed: () => onClose(context),
child: !isMobileView ? _closeButton : _backButton,
),
),
),
),
);
}
@override
Widget body(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, buySellViewModel));
return KeyboardActions(
disableScroll: true,
config: KeyboardActionsConfig(
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
keyboardBarColor: Theme.of(context).extension<KeyboardTheme>()!.keyboardBarColor,
nextFocus: false,
actions: [
KeyboardActionsItem(
focusNode: _fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
KeyboardActionsItem(
focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
]),
child: Container(
color: Theme.of(context).colorScheme.background,
child: Form(
key: _formKey,
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(bottom: 24),
content: Observer(
builder: (_) => Column(children: [
_exchangeCardsSection(context),
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
SizedBox(height: 12),
_buildPaymentMethodTile(context),
],
),
),
])),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: Column(children: [
Observer(
builder: (_) => LoadingPrimaryButton(
text: S.current.choose_a_provider,
onPressed: () async {
if(!_formKey.currentState!.validate()) return;
buySellViewModel.onTapChoseProvider(context);
},
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isDisabled: false,
isLoading: !buySellViewModel.isReadyToTrade)),
]),
)),
));
}
Widget _buildPaymentMethodTile(BuildContext context) {
if (buySellViewModel.paymentMethodState is PaymentMethodLoading ||
buySellViewModel.paymentMethodState is InitialPaymentMethod) {
return OptionTilePlaceholder(
withBadge: false,
withSubtitle: false,
borderRadius: 30,
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
leadingIcon: Icons.arrow_forward_ios,
isDarkTheme: buySellViewModel.isDarkTheme);
}
if (buySellViewModel.paymentMethodState is PaymentMethodFailed) {
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
}
if (buySellViewModel.paymentMethodState is PaymentMethodLoaded &&
buySellViewModel.selectedPaymentMethod != null) {
return Observer(builder: (_) {
final selectedPaymentMethod = buySellViewModel.selectedPaymentMethod!;
return ProviderOptionTile(
lightImagePath: selectedPaymentMethod.lightIconPath,
darkImagePath: selectedPaymentMethod.darkIconPath,
title: selectedPaymentMethod.title,
onPressed: () => _pickPaymentMethod(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 payment methods available', borderRadius: 30);
}
void _pickPaymentMethod(BuildContext context) async {
final currentOption = buySellViewModel.selectedPaymentMethod;
await Navigator.of(context).pushNamed(
Routes.paymentMethodOptionsPage,
arguments: [
buySellViewModel.paymentMethods,
buySellViewModel.changeOption,
],
);
buySellViewModel.selectedPaymentMethod;
if (currentOption != null &&
currentOption.paymentMethodType !=
buySellViewModel.selectedPaymentMethod?.paymentMethodType) {
await buySellViewModel.calculateBestRate();
}
}
void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) {
if (_isReactionsSet) {
return;
}
final fiatAmountController = fiatCurrencyKey.currentState!.amountController;
final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController;
final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController;
_onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey);
_onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey);
reaction(
(_) => buySellViewModel.wallet.name,
(String _) =>
_onWalletNameChange(buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey));
reaction(
(_) => buySellViewModel.cryptoCurrency,
(CryptoCurrency currency) =>
_onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey));
reaction(
(_) => buySellViewModel.fiatCurrency,
(FiatCurrency currency) =>
_onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey));
reaction((_) => buySellViewModel.fiatAmount, (String amount) {
if (fiatCurrencyKey.currentState!.amountController.text != amount) {
fiatCurrencyKey.currentState!.amountController.text = amount;
}
});
reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (bool isEnabled) {
cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled);
});
reaction((_) => buySellViewModel.cryptoAmount, (String amount) {
if (cryptoCurrencyKey.currentState!.amountController.text != amount) {
cryptoCurrencyKey.currentState!.amountController.text = amount;
}
});
reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) {
if (cryptoAddressController != address) {
cryptoCurrencyKey.currentState!.addressController.text = address;
}
});
fiatAmountController.addListener(() {
if (fiatAmountController.text != buySellViewModel.fiatAmount) {
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
}
});
cryptoAmountController.addListener(() {
if (cryptoAmountController.text != buySellViewModel.cryptoAmount) {
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
}
});
cryptoAddressController.addListener(() {
buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text);
});
_cryptoAddressFocus.addListener(() async {
if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) {
final domain = cryptoAddressController.text;
buySellViewModel.cryptoCurrencyAddress =
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
}
});
reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) {
if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) {
cryptoCurrencyKey.currentState!.changeAddress(address: address);
}
});
reaction((_) => buySellViewModel.isReadyToTrade, (bool isReady) {
if (isReady) {
if (cryptoAmountController.text.isNotEmpty &&
cryptoAmountController.text != S.current.fetching) {
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
} else if (fiatAmountController.text.isNotEmpty &&
fiatAmountController.text != S.current.fetching) {
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
}
}
});
_isReactionsSet = true;
}
void _onCurrencyChange(Currency currency, BuySellViewModel buySellViewModel,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
key.currentState!.changeSelectedCurrency(currency);
key.currentState!.changeWalletName(isCurrentTypeWallet ? buySellViewModel.wallet.name : '');
key.currentState!.changeAddress(
address: isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : '');
key.currentState!.changeAmount(amount: '');
}
void _onWalletNameChange(BuySellViewModel buySellViewModel, CryptoCurrency currency,
GlobalKey<ExchangeCardState> key) {
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
if (isCurrentTypeWallet) {
key.currentState!.changeWalletName(buySellViewModel.wallet.name);
key.currentState!.addressController.text = buySellViewModel.wallet.walletAddresses.addressForExchange;
} else if (key.currentState!.addressController.text ==
buySellViewModel.wallet.walletAddresses.addressForExchange) {
key.currentState!.changeWalletName('');
key.currentState!.addressController.text = '';
}
}
void disposeBestRateSync() => {};
Widget _exchangeCardsSection(BuildContext context) {
final fiatExchangeCard = Observer(
builder: (_) => ExchangeCard(
cardInstanceName: 'fiat_currency_trade_card',
onDispose: disposeBestRateSync,
amountFocusNode: _fiatAmountFocus,
key: fiatCurrencyKey,
title: 'FIAT ${S.of(context).amount}',
initialCurrency: buySellViewModel.fiatCurrency,
initialWalletName: '',
initialAddress: '',
initialIsAmountEditable: true,
isAmountEstimated: false,
currencyRowPadding: EdgeInsets.zero,
addressRowPadding: EdgeInsets.zero,
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
showAddressField: false,
showLimitsField: false,
currencies: buySellViewModel.fiatCurrencies,
onCurrencySelected: (currency) =>
buySellViewModel.changeFiatCurrency(currency: currency),
imageArrow: arrowBottomPurple,
currencyButtonColor: Colors.transparent,
addressButtonsColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderTopPanelColor,
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
));
final cryptoExchangeCard = Observer(
builder: (_) => ExchangeCard(
cardInstanceName: 'crypto_currency_trade_card',
onDispose: disposeBestRateSync,
amountFocusNode: _cryptoAmountFocus,
addressFocusNode: _cryptoAddressFocus,
key: cryptoCurrencyKey,
title: 'Crypto ${S.of(context).amount}',
initialCurrency: buySellViewModel.cryptoCurrency,
initialWalletName: '',
initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency
? buySellViewModel.wallet.walletAddresses.addressForExchange
: buySellViewModel.cryptoCurrencyAddress,
initialIsAmountEditable: true,
isAmountEstimated: true,
showLimitsField: false,
currencyRowPadding: EdgeInsets.zero,
addressRowPadding: EdgeInsets.zero,
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
currencies: buySellViewModel.cryptoCurrencies,
onCurrencySelected: (currency) =>
buySellViewModel.changeCryptoCurrency(currency: currency),
imageArrow: arrowBottomCakeGreen,
currencyButtonColor: Colors.transparent,
addressButtonsColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<ExchangePageTheme>()!.textFieldBorderBottomPanelColor,
addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency),
onPushPasteButton: (context) async {},
onPushAddressBookButton: (context) async {},
));
if (responsiveLayoutUtil.shouldRenderMobileUI) {
return Observer(
builder: (_) {
if (buySellViewModel.isBuyAction) {
return MobileExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
onBuyTap: () => null,
onSellTap: () =>
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
isBuySellOption: true,
);
} else {
return MobileExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
onBuyTap: () =>
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
onSellTap: () => null,
isBuySellOption: true,
);
}
},
);
}
return Observer(
builder: (_) {
if (buySellViewModel.isBuyAction) {
return DesktopExchangeCardsSection(
firstExchangeCard: fiatExchangeCard,
secondExchangeCard: cryptoExchangeCard,
);
} else {
return DesktopExchangeCardsSection(
firstExchangeCard: cryptoExchangeCard,
secondExchangeCard: fiatExchangeCard,
);
}
},
);
}
Future<String> fetchParsedAddress(
BuildContext context, String domain, CryptoCurrency currency) async {
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
final address = await extractAddressFromParsed(context, parsedAddress);
return address;
}
}
@@ -0,0 +1,47 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/select_options_page.dart';
import 'package:flutter/cupertino.dart';
class PaymentMethodOptionsPage extends SelectOptionsPage {
PaymentMethodOptionsPage({required this.items, this.pickAnOption});
final List<SelectableItem> items;
final Function(SelectableOption option)? pickAnOption;
@override
String get pageTitle => S.current.choose_a_payment_method;
@override
EdgeInsets? get contentPadding => null;
@override
EdgeInsets? get tilePadding => EdgeInsets.only(top: 12);
@override
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 12);
@override
double? get imageHeight => null;
@override
double? get imageWidth => null;
@override
Color? get selectedBackgroundColor => null;
@override
double? get tileBorderRadius => 30;
@override
String get bottomSectionText => '';
@override
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
@override
String get primaryButtonText => S.current.confirm;
@override
void Function(BuildContext context)? get primaryButtonAction => null;
}
@@ -21,6 +21,14 @@ class DesktopDashboardActions extends StatelessWidget {
return Column(
children: [
const SizedBox(height: 16),
DesktopActionButton(
title: MainActions.showWalletsAction.name(context),
image: MainActions.showWalletsAction.image,
canShow: MainActions.showWalletsAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.showWalletsAction.isEnabled?.call(dashboardViewModel),
onTap: () async =>
await MainActions.showWalletsAction.onTap(context, dashboardViewModel),
),
DesktopActionButton(
title: MainActions.exchangeAction.name(context),
image: MainActions.exchangeAction.image,
@@ -55,20 +63,11 @@ class DesktopDashboardActions extends StatelessWidget {
children: [
Expanded(
child: DesktopActionButton(
title: MainActions.buyAction.name(context),
image: MainActions.buyAction.image,
canShow: MainActions.buyAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.buyAction.isEnabled?.call(dashboardViewModel),
onTap: () async => await MainActions.buyAction.onTap(context, dashboardViewModel),
),
),
Expanded(
child: DesktopActionButton(
title: MainActions.sellAction.name(context),
image: MainActions.sellAction.image,
canShow: MainActions.sellAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.sellAction.isEnabled?.call(dashboardViewModel),
onTap: () async => await MainActions.sellAction.onTap(context, dashboardViewModel),
title: MainActions.tradeAction.name(context),
image: MainActions.tradeAction.image,
canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel),
isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel),
onTap: () async => await MainActions.tradeAction.onTap(context, dashboardViewModel),
),
),
],
@@ -18,7 +18,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
class ExchangeCard extends StatefulWidget {
class ExchangeCard<T extends Currency> extends StatefulWidget {
ExchangeCard({
Key? key,
required this.initialCurrency,
@@ -40,19 +40,23 @@ class ExchangeCard extends StatefulWidget {
this.borderColor = Colors.transparent,
this.hasAllAmount = false,
this.isAllAmountEnabled = false,
this.showAddressField = true,
this.showLimitsField = true,
this.amountFocusNode,
this.addressFocusNode,
this.allAmount,
this.currencyRowPadding,
this.addressRowPadding,
this.onPushPasteButton,
this.onPushAddressBookButton,
this.onDispose,
required this.cardInstanceName,
}) : super(key: key);
final List<CryptoCurrency> currencies;
final Function(CryptoCurrency) onCurrencySelected;
final List<T> currencies;
final Function(T) onCurrencySelected;
final String title;
final CryptoCurrency initialCurrency;
final T initialCurrency;
final String initialWalletName;
final String initialAddress;
final bool initialIsAmountEditable;
@@ -70,18 +74,22 @@ class ExchangeCard extends StatefulWidget {
final FocusNode? amountFocusNode;
final FocusNode? addressFocusNode;
final bool hasAllAmount;
final bool showAddressField;
final bool showLimitsField;
final bool isAllAmountEnabled;
final VoidCallback? allAmount;
final EdgeInsets? currencyRowPadding;
final EdgeInsets? addressRowPadding;
final void Function(BuildContext context)? onPushPasteButton;
final void Function(BuildContext context)? onPushAddressBookButton;
final Function()? onDispose;
final String cardInstanceName;
@override
ExchangeCardState createState() => ExchangeCardState();
ExchangeCardState<T> createState() => ExchangeCardState<T>();
}
class ExchangeCardState extends State<ExchangeCard> {
class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
ExchangeCardState()
: _title = '',
_min = '',
@@ -89,7 +97,6 @@ class ExchangeCardState extends State<ExchangeCard> {
_isAmountEditable = false,
_isAddressEditable = false,
_walletName = '',
_selectedCurrency = CryptoCurrency.btc,
_isAmountEstimated = false,
_isMoneroWallet = false,
_cardInstanceName = '';
@@ -101,7 +108,7 @@ class ExchangeCardState extends State<ExchangeCard> {
String _title;
String? _min;
String? _max;
CryptoCurrency _selectedCurrency;
late T _selectedCurrency;
String _walletName;
bool _isAmountEditable;
bool _isAddressEditable;
@@ -118,7 +125,8 @@ class ExchangeCardState extends State<ExchangeCard> {
_selectedCurrency = widget.initialCurrency;
_isAmountEstimated = widget.isAmountEstimated;
_isMoneroWallet = widget.isMoneroWallet;
addressController.text = widget.initialAddress;
addressController.text = _normalizeAddressFormat(widget.initialAddress);
super.initState();
}
@@ -136,7 +144,7 @@ class ExchangeCardState extends State<ExchangeCard> {
});
}
void changeSelectedCurrency(CryptoCurrency currency) {
void changeSelectedCurrency(T currency) {
setState(() => _selectedCurrency = currency);
}
@@ -157,7 +165,7 @@ class ExchangeCardState extends State<ExchangeCard> {
}
void changeAddress({required String address}) {
setState(() => addressController.text = address);
setState(() => addressController.text = _normalizeAddressFormat(address));
}
void changeAmount({required String amount}) {
@@ -222,7 +230,7 @@ class ExchangeCardState extends State<ExchangeCard> {
Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Padding(
padding: EdgeInsets.only(top: 5),
child: Container(
child: widget.showLimitsField ? Container(
height: 15,
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
_min != null
@@ -247,7 +255,7 @@ class ExchangeCardState extends State<ExchangeCard> {
),
)
: Offstage(),
])),
])) : Offstage(),
),
!_isAddressEditable && widget.hasRefundAddress
? Padding(
@@ -261,10 +269,11 @@ class ExchangeCardState extends State<ExchangeCard> {
))
: Offstage(),
_isAddressEditable
? widget.showAddressField
? FocusTraversalOrder(
order: NumericFocusOrder(2),
child: Padding(
padding: EdgeInsets.only(top: 20),
padding: widget.addressRowPadding ?? EdgeInsets.only(top: 20),
child: AddressTextField(
addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
focusNode: widget.addressFocusNode,
@@ -280,26 +289,29 @@ class ExchangeCardState extends State<ExchangeCard> {
widget.amountFocusNode?.requestFocus();
amountController.text = paymentRequest.amount;
},
placeholder: widget.hasRefundAddress ? S.of(context).refund_address : null,
placeholder:
widget.hasRefundAddress ? S.of(context).refund_address : null,
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook,
],
isBorderExist: false,
textStyle:
TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
textStyle: TextStyle(
fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white),
hintStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
color:
Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor),
buttonColor: widget.addressButtonsColor,
validator: widget.addressTextFieldValidator,
onPushPasteButton: widget.onPushPasteButton,
onPushAddressBookButton: widget.onPushAddressBookButton,
selectedCurrency: _selectedCurrency),
),
)
)
: Offstage()
: Padding(
padding: EdgeInsets.only(top: 10),
child: Builder(
@@ -402,7 +414,7 @@ class ExchangeCardState extends State<ExchangeCard> {
hintText: S.of(context).search_currency,
isMoneroWallet: _isMoneroWallet,
isConvertFrom: widget.hasRefundAddress,
onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency),
onItemSelected: (Currency item) => widget.onCurrencySelected(item as T),
),
);
}
@@ -424,4 +436,10 @@ class ExchangeCardState extends State<ExchangeCard> {
actionLeftButton: () => Navigator.of(dialogContext).pop());
});
}
String _normalizeAddressFormat(String address) {
if (address.startsWith('bitcoincash:')) address = address.substring(12);
return address;
}
}
@@ -1,20 +1,29 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter/material.dart';
class MobileExchangeCardsSection extends StatelessWidget {
final Widget firstExchangeCard;
final Widget secondExchangeCard;
final bool isBuySellOption;
final VoidCallback? onBuyTap;
final VoidCallback? onSellTap;
const MobileExchangeCardsSection({
Key? key,
required this.firstExchangeCard,
required this.secondExchangeCard,
this.isBuySellOption = false,
this.onBuyTap,
this.onSellTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: 32),
padding: EdgeInsets.only(bottom: isBuySellOption ? 8 : 32),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24),
@@ -45,8 +54,18 @@ class MobileExchangeCardsSection extends StatelessWidget {
end: Alignment.bottomRight,
),
),
padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
child: firstExchangeCard,
padding: EdgeInsets.fromLTRB(24, 90, 24, isBuySellOption ? 8 : 32),
child: Column(
children: [
if (isBuySellOption) Column(
children: [
const SizedBox(height: 16),
BuySellOptionButtons(onBuyTap: onBuyTap, onSellTap: onSellTap),
],
),
firstExchangeCard,
],
),
),
Padding(
padding: EdgeInsets.only(top: 29, left: 24, right: 24),
@@ -57,3 +76,69 @@ class MobileExchangeCardsSection extends StatelessWidget {
);
}
}
class BuySellOptionButtons extends StatefulWidget {
final VoidCallback? onBuyTap;
final VoidCallback? onSellTap;
const BuySellOptionButtons({this.onBuyTap, this.onSellTap});
@override
_BuySellOptionButtonsState createState() => _BuySellOptionButtonsState();
}
class _BuySellOptionButtonsState extends State<BuySellOptionButtons> {
bool isBuySelected = true;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Row(
children: [
Expanded(flex: 2, child: SizedBox()),
Expanded(
flex: 5,
child: SelectButton(
height: 44,
text: S.of(context).buy,
isSelected: isBuySelected,
showTrailingIcon: false,
textColor: Colors.white,
image: Image.asset('assets/images/buy.png', height: 25, width: 25),
padding: EdgeInsets.only(left: 10, right: 30),
color: isBuySelected
? null
: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
onTap: () {
setState(() => isBuySelected = true);
if (widget.onBuyTap != null) widget.onBuyTap!();
},
),
),
Expanded(child: const SizedBox()),
Expanded(
flex: 5,
child: SelectButton(
height: 44,
text: S.of(context).sell,
isSelected: !isBuySelected,
showTrailingIcon: false,
textColor: Colors.white,
image: Image.asset('assets/images/sell.png', height: 25, width: 25),
padding: EdgeInsets.only(left: 10, right: 30),
color: !isBuySelected
? null
: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
onTap: () {
setState(() => isBuySelected = false);
if (widget.onSellTap != null) widget.onSellTap!();
},
),
),
Expanded(flex: 2, child: SizedBox()),
],
),
);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/InfoPage.dart';
import 'package:cake_wallet/src/screens/Info_page.dart';
import 'package:flutter/cupertino.dart';
class PreSeedPage extends InfoPage {
+199
View File
@@ -0,0 +1,199 @@
import 'package:cake_wallet/core/selectable_option.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/provider_optoin_tile.dart';
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:flutter/material.dart';
abstract class SelectOptionsPage extends BasePage {
SelectOptionsPage();
String get pageTitle;
EdgeInsets? get contentPadding;
EdgeInsets? get tilePadding;
EdgeInsets? get innerPadding;
double? get imageHeight;
double? get imageWidth;
Color? get selectedBackgroundColor;
double? get tileBorderRadius;
String get bottomSectionText;
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;
@override
Widget body(BuildContext context) {
return ScrollableWithBottomSection(
content: BodySelectOptionsPage(
items: items,
onOptionTap: onOptionTap,
tilePadding: tilePadding,
tileBorderRadius: tileBorderRadius,
imageHeight: imageHeight,
imageWidth: imageWidth,
innerPadding: innerPadding),
bottomSection: Padding(
padding: contentPadding ?? EdgeInsets.zero,
child: Column(
children: [
Text(
bottomSectionText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
),
if (primaryButtonEnabled)
LoadingPrimaryButton(
text: primaryButtonText,
onPressed: () {
primaryButtonAction != null
? primaryButtonAction!(context)
: Navigator.pop(context);
},
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isDisabled: false,
isLoading: false)
],
),
),
);
}
}
class BodySelectOptionsPage extends StatefulWidget {
const BodySelectOptionsPage({
required this.items,
this.onOptionTap,
this.tilePadding,
this.tileBorderRadius,
this.imageHeight,
this.imageWidth,
this.innerPadding,
});
final List<SelectableItem> items;
final void Function(SelectableOption option)? onOptionTap;
final EdgeInsets? tilePadding;
final double? tileBorderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? innerPadding;
@override
_BodySelectOptionsPageState createState() => _BodySelectOptionsPageState();
}
class _BodySelectOptionsPageState extends State<BodySelectOptionsPage> {
late List<SelectableItem> _items;
@override
void initState() {
super.initState();
_items = widget.items;
}
void _handleOptionTap(SelectableOption option) {
setState(() {
for (var item in _items) {
if (item is SelectableOption) {
item.isOptionSelected = false;
}
}
option.isOptionSelected = true;
});
widget.onOptionTap?.call(option);
}
@override
Widget build(BuildContext context) {
final isLightMode = Theme.of(context).extension<OptionTileTheme>()?.useDarkImage ?? false;
Color titleColor =
isLightMode ? Theme.of(context).appBarTheme.titleTextStyle!.color! : Colors.white;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 350),
child: Column(
children: _items.map((item) {
if (item is OptionTitle) {
return Padding(
padding: const EdgeInsets.only(top: 18, bottom: 8),
child: Container(
width: double.infinity,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: titleColor,
width: 1,
),
),
),
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
item.title,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: titleColor,
),
),
),
),
);
} else if (item is SelectableOption) {
return Padding(
padding: widget.tilePadding ?? const EdgeInsets.only(top: 24),
child: ProviderOptionTile(
title: item.title,
lightImagePath: item.lightIconPath,
darkImagePath: item.darkIconPath,
imageHeight: widget.imageHeight,
imageWidth: widget.imageWidth,
padding: widget.innerPadding,
description: item.description,
topLeftSubTitle: item.topLeftSubTitle,
topRightSubTitle: item.topRightSubTitle,
rightSubTitleLightIconPath: item.topRightSubTitleLightIconPath,
rightSubTitleDarkIconPath: item.topRightSubTitleDarkIconPath,
bottomLeftSubTitle: item.bottomLeftSubTitle,
badges: item.badges,
isSelected: item.isOptionSelected,
borderRadius: widget.tileBorderRadius,
isLightMode: isLightMode,
onPressed: () => _handleOptionTap(item),
),
);
}
return const SizedBox.shrink();
}).toList(),
),
),
);
}
}
@@ -57,22 +57,6 @@ class OtherSettingsPage extends BasePage {
handler: (BuildContext context) =>
Navigator.of(context).pushNamed(Routes.changeRep),
),
if(_otherSettingsViewModel.isEnabledBuyAction)
SettingsPickerCell(
title: S.current.default_buy_provider,
items: _otherSettingsViewModel.availableBuyProvidersTypes,
displayItem: _otherSettingsViewModel.getBuyProviderType,
selectedItem: _otherSettingsViewModel.buyProviderType,
onItemSelected: _otherSettingsViewModel.onBuyProviderTypeSelected
),
if(_otherSettingsViewModel.isEnabledSellAction)
SettingsPickerCell(
title: S.current.default_sell_provider,
items: _otherSettingsViewModel.availableSellProvidersTypes,
displayItem: _otherSettingsViewModel.getSellProviderType,
selectedItem: _otherSettingsViewModel.sellProviderType,
onItemSelected: _otherSettingsViewModel.onSellProviderTypeSelected,
),
SettingsCellWithArrow(
title: S.current.settings_terms_and_conditions,
handler: (BuildContext context) =>
+3 -9
View File
@@ -73,16 +73,10 @@ class PrivacyPage extends BasePage {
_privacySettingsViewModel.setIsAppSecure(value);
}),
SettingsSwitcherCell(
title: S.current.disable_buy,
value: _privacySettingsViewModel.disableBuy,
title: S.current.disable_trade_option,
value: _privacySettingsViewModel.disableTradeOption,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableBuy(value);
}),
SettingsSwitcherCell(
title: S.current.disable_sell,
value: _privacySettingsViewModel.disableSell,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setDisableSell(value);
_privacySettingsViewModel.setDisableTradeOption(value);
}),
SettingsSwitcherCell(
title: S.current.disable_bulletin,
@@ -1,6 +1,6 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/InfoPage.dart';
import 'package:cake_wallet/src/screens/Info_page.dart';
import 'package:flutter/cupertino.dart';
class Setup2FAInfoPage extends InfoPage {
+4 -3
View File
@@ -1,20 +1,21 @@
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:permission_handler/permission_handler.dart';
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
class AddressTextField extends StatelessWidget {
class AddressTextField<T extends Currency> extends StatelessWidget{
AddressTextField({
required this.controller,
this.isActive = true,
@@ -58,7 +59,7 @@ class AddressTextField extends StatelessWidget {
final Function(BuildContext context)? onPushAddressBookButton;
final Function(BuildContext context)? onPushAddressPickerButton;
final Function(ContactBase contact)? onSelectedContact;
final CryptoCurrency? selectedCurrency;
final T? selectedCurrency;
final Key? addressKey;
@override
+527
View File
@@ -0,0 +1,527 @@
import 'package:cake_wallet/themes/extensions/option_tile_theme.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:cake_wallet/typography.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class ProviderOptionTile extends StatelessWidget {
const ProviderOptionTile({
required this.onPressed,
required this.lightImagePath,
required this.darkImagePath,
required this.title,
this.topLeftSubTitle,
this.topRightSubTitle,
this.bottomLeftSubTitle,
this.bottomRightSubTitle,
this.leftSubTitleIconPath,
this.rightSubTitleLightIconPath,
this.rightSubTitleDarkIconPath,
this.description,
this.badges,
this.borderRadius,
this.imageHeight,
this.imageWidth,
this.padding,
this.titleTextStyle,
this.firstSubTitleTextStyle,
this.secondSubTitleTextStyle,
this.leadingIcon,
this.selectedBackgroundColor,
this.isSelected = false,
required this.isLightMode,
});
final VoidCallback onPressed;
final String lightImagePath;
final String darkImagePath;
final String title;
final String? topLeftSubTitle;
final String? topRightSubTitle;
final String? bottomLeftSubTitle;
final String? bottomRightSubTitle;
final String? leftSubTitleIconPath;
final String? rightSubTitleLightIconPath;
final String? rightSubTitleDarkIconPath;
final String? description;
final List<String>? badges;
final double? borderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? padding;
final TextStyle? titleTextStyle;
final TextStyle? firstSubTitleTextStyle;
final TextStyle? secondSubTitleTextStyle;
final IconData? leadingIcon;
final Color? selectedBackgroundColor;
final bool isSelected;
final bool isLightMode;
@override
Widget build(BuildContext context) {
final backgroundColor = isSelected
? isLightMode
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor
: Theme.of(context).cardColor;
final textColor = isSelected
? isLightMode
? Colors.white
: Theme.of(context).cardColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor;
final badgeColor = isSelected
? Theme.of(context).cardColor
: Theme.of(context).extension<OptionTileTheme>()!.titleColor;
final badgeTextColor = isSelected
? Theme.of(context).extension<OptionTileTheme>()!.titleColor
: Theme.of(context).cardColor;
final imagePath = isSelected
? isLightMode
? darkImagePath
: lightImagePath
: isLightMode
? lightImagePath
: darkImagePath;
final rightSubTitleIconPath = isSelected
? isLightMode
? rightSubTitleDarkIconPath
: rightSubTitleLightIconPath
: isLightMode
? rightSubTitleLightIconPath
: rightSubTitleDarkIconPath;
return GestureDetector(
onTap: onPressed,
child: Container(
width: double.infinity,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(borderRadius ?? 12)),
border: isSelected && !isLightMode ? Border.all(color: textColor) : null,
color: backgroundColor,
),
child: Padding(
padding: padding ?? const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
getImage(imagePath, height: imageHeight, width: imageWidth),
SizedBox(width: 8),
Expanded(
child: Container(
child: Row(
children: [
Expanded(
child: Text(title,
style: titleTextStyle ?? textLargeBold(color: textColor))),
Row(
children: [
if (leadingIcon != null)
Icon(leadingIcon, size: 16, color: textColor),
],
)
],
),
),
),
],
),
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),
child: Row(children: [
...badges!
.map((badge) => Badge(
title: badge, textColor: badgeTextColor, backgroundColor: badgeColor))
.toList()
]),
)
],
),
),
),
);
}
}
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});
final String title;
final Color textColor;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FittedBox(
fit: BoxFit.fitHeight,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(24)), color: backgroundColor),
alignment: Alignment.center,
child: Text(
title,
style: TextStyle(
color: textColor,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
}
Widget getImage(String imagePath, {double? height, double? width, Color? imageColor}) {
final bool isNetworkImage = imagePath.startsWith('http') || imagePath.startsWith('https');
final bool isSvg = imagePath.endsWith('.svg');
final double imageHeight = height ?? 35;
final double imageWidth = width ?? 35;
if (isNetworkImage) {
return isSvg
? SvgPicture.network(
imagePath,
height: imageHeight,
width: imageWidth,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
placeholderBuilder: (BuildContext context) => Container(
height: imageHeight,
width: imageWidth,
child: Center(
child: CircularProgressIndicator(),
),
),
)
: Image.network(
imagePath,
height: imageHeight,
width: imageWidth,
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child;
}
return Container(
height: imageHeight,
width: imageWidth,
child: Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
),
);
},
errorBuilder: (BuildContext context, Object exception, StackTrace? stackTrace) {
return Container(
height: imageHeight,
width: imageWidth,
);
},
);
} else {
return isSvg
? SvgPicture.asset(
imagePath,
height: imageHeight,
width: imageWidth,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
)
: Image.asset(imagePath, height: imageHeight, width: imageWidth);
}
}
class OptionTilePlaceholder extends StatefulWidget {
OptionTilePlaceholder({
this.borderRadius,
this.imageHeight,
this.imageWidth,
this.padding,
this.leadingIcon,
this.withBadge = true,
this.withSubtitle = true,
this.isDarkTheme = false,
this.errorText,
});
final double? borderRadius;
final double? imageHeight;
final double? imageWidth;
final EdgeInsets? padding;
final IconData? leadingIcon;
final bool withBadge;
final bool withSubtitle;
final bool isDarkTheme;
final String? errorText;
@override
_OptionTilePlaceholderState createState() => _OptionTilePlaceholderState();
}
class _OptionTilePlaceholderState extends State<OptionTilePlaceholder>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat();
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.linear,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final backgroundColor = Theme.of(context).cardColor;
final titleColor = Theme.of(context).extension<OptionTileTheme>()!.titleColor.withOpacity(0.4);
return widget.errorText != null
? Container(
width: double.infinity,
padding: widget.padding ?? EdgeInsets.all(16),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
color: backgroundColor,
),
child: Column(
children: [
Text(
widget.errorText!,
style: TextStyle(
color: titleColor,
fontSize: 16,
),
),
if (widget.withSubtitle) SizedBox(height: 8),
Text(
'',
style: TextStyle(
color: titleColor,
fontSize: 16,
),
),
],
),
)
: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Stack(
children: [
Container(
width: double.infinity,
padding: widget.padding ?? EdgeInsets.all(16),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
color: backgroundColor,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
children: [
Container(
height: widget.imageHeight ?? 35,
width: widget.imageWidth ?? 35,
decoration: BoxDecoration(
color: titleColor,
shape: BoxShape.circle,
),
),
SizedBox(width: 8),
Expanded(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 20,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
if (widget.leadingIcon != null)
Icon(widget.leadingIcon, size: 16, color: titleColor),
],
),
),
),
],
),
if (widget.withSubtitle)
Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
height: 20,
width: 170,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
),
],
),
),
if (widget.withBadge)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Row(
children: [
Container(
height: 30,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
),
SizedBox(width: 8),
Container(
height: 30,
width: 70,
decoration: BoxDecoration(
color: titleColor,
borderRadius: BorderRadius.all(Radius.circular(20)),
),
),
],
),
),
],
),
),
Positioned.fill(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(widget.borderRadius ?? 12)),
gradient: LinearGradient(
begin: Alignment(-2, -4),
end: Alignment(2, 4),
stops: [
_animation.value - 0.2,
_animation.value,
_animation.value + 0.2,
],
colors: [
backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
backgroundColor.withOpacity(widget.isDarkTheme ? 0.7 : 0.4),
backgroundColor.withOpacity(widget.isDarkTheme ? 0.4 : 0.7),
],
),
),
),
),
],
);
},
);
}
}