mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-11-09 08:24:08 +01:00
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:
@@ -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;
|
||||
}
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user