Merge branch 'dev' into cw-1440-new-buy-sell-flow

This commit is contained in:
malik1004x
2026-08-20 16:26:13 +02:00
committed by GitHub
220 changed files with 11956 additions and 9280 deletions
File diff suppressed because it is too large Load Diff
+36 -8
View File
@@ -7,6 +7,7 @@ import "package:cake_wallet/exchange/exchange_trade_state.dart";
import "package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart";
import "package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/new-ui/widgets/keyboard_hide_overlay.dart";
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
@@ -27,6 +28,7 @@ import "package:cake_wallet/src/widgets/primary_button.dart";
import "package:cake_wallet/utils/debounce.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
import "package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart";
import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
@@ -51,6 +53,7 @@ class NewSwapPage extends StatefulWidget {
required this.walletSwitcherViewModel,
CryptoCurrency? initialCurrency,
this.fromSend,
this.balanceViewModel,
}) {
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
? exchangeViewModel.wallet.name
@@ -69,6 +72,7 @@ class NewSwapPage extends StatefulWidget {
final AddressResolverService adrResService;
final PaymentRequest? initialPaymentRequest;
final SwapFromSendArgs? fromSend;
final BalanceViewModel? balanceViewModel;
late final String? depositWalletName;
late final String? receiveWalletName;
@@ -93,6 +97,25 @@ class _NewSwapPageState extends State<NewSwapPage> {
.contains(widget.exchangeViewModel.depositCurrency) &&
!(widget.exchangeViewModel.status is SyncedSyncStatus);
Map<CryptoCurrency, CurrencyPickerBalance>? _depositBalanceByAsset() {
final balanceViewModel = widget.balanceViewModel;
if (balanceViewModel == null) {
return null;
}
return {
for (final r in balanceViewModel.formattedBalances)
r.asset: CurrencyPickerBalance(
amount: "${r.availableBalance} ${r.asset.title}",
fiat: balanceViewModel.isFiatDisabled
? null
: "${r.fiatAvailableBalanceRaw} ${r.fiatCurrency?.symbol}",
fiatValue:
balanceViewModel.isFiatDisabled ? null : double.tryParse(r.fiatAvailableBalanceRaw),
),
};
}
@override
void initState() {
super.initState();
@@ -258,8 +281,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
(bool isReceiveAmountEditable) {},
));
_disposers
.add(reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) {
_disposers.add(
reaction((_) => widget.exchangeViewModel.tradeState, (ExchangeTradeState state) async {
if (state is TradeIsCreatedFailure) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
@@ -298,11 +321,15 @@ class _NewSwapPageState extends State<NewSwapPage> {
exchangeTradeViewModel: vm,
receiveAmount: receiveAmount,
);
showMaterialModalBottomSheet(
context: context,
builder: (context) => page,
backgroundColor: Colors.transparent,
);
try {
await showMaterialModalBottomSheet<void>(
context: context,
builder: (context) => page,
backgroundColor: Colors.transparent,
);
} finally {
widget.exchangeViewModel.tradeStarted = false;
}
}
}));
@@ -629,7 +656,8 @@ class _NewSwapPageState extends State<NewSwapPage> {
walletName: fromSend != null
? widget.exchangeViewModel.wallet.name
: null,
balanceByAsset: fromSend?.depositBalanceByAsset,
balanceByAsset:
fromSend != null ? _depositBalanceByAsset() : null,
useSingleNetworkLayout: fromSend != null,
filteredNetwork: fromSend != null
? widget.exchangeViewModel.wallet.type
@@ -0,0 +1,29 @@
import "package:cake_wallet/core/wallet_loading_service.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cw_core/exceptions.dart";
import "package:cw_core/wallet_info.dart";
class WalletSwitchService {
WalletSwitchService({
required WalletLoadingService walletLoadingService,
required AppStore appStore,
}) : _walletLoadingService = walletLoadingService,
_appStore = appStore;
final AppStore _appStore;
final WalletLoadingService _walletLoadingService;
Future<void> switchToWallet(WalletInfo walletInfo) async {
final wallet = await _walletLoadingService.load(walletInfo.type, walletInfo.name);
// load() recovers from a corrupted wallet by opening any other wallet it can,
// so the one it returns may not be the one that was requested.
if (wallet.name != walletInfo.name || wallet.type != walletInfo.type) {
throw WalletSwitchException(
"wallet switch loaded ${wallet.name} instead of ${walletInfo.name}",
);
}
await _appStore.changeCurrentWallet(wallet);
}
}
+391
View File
@@ -0,0 +1,391 @@
import "package:cake_wallet/core/address_resolver/address_resolver_service.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_service.dart";
import "package:cake_wallet/core/auth_service.dart";
import "package:cake_wallet/di.dart";
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/pages/swap_page.dart";
import "package:cake_wallet/new-ui/widgets/anypay/evm_address_detected_sheet.dart";
import "package:cake_wallet/new-ui/widgets/anypay/network_decision_page.dart";
import "package:cake_wallet/new-ui/widgets/anypay/select_recipient_network_sheet.dart";
import "package:cake_wallet/new-ui/widgets/anypay/switch_network_wallet_page.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_from_send_args.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/src/widgets/alert_with_one_action.dart";
import "package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart";
import "package:cake_wallet/utils/show_pop_up.dart";
import "package:cake_wallet/view_model/exchange/exchange_view_model.dart";
import "package:cake_wallet/view_model/send/send_view_model.dart";
import "package:cake_wallet/view_model/wallet_switcher_view_model.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/unspent_coin_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_info.dart";
import "package:flutter/cupertino.dart";
import "package:flutter/material.dart";
class AnyPayFormFill {
const AnyPayFormFill({
required this.request,
this.token,
this.fallbackCurrency,
this.amountOverride,
});
final AnyPayRequest request;
final CryptoCurrency? token;
final CryptoCurrency? fallbackCurrency;
final String? amountOverride;
}
class AnyPayFlow {
AnyPayFlow({
required this.anyPayService,
required this.sendViewModel,
required this.authService,
required this.walletSwitcherViewModel,
});
final AnyPayService anyPayService;
final SendViewModel sendViewModel;
final AuthService authService;
final WalletSwitcherViewModel walletSwitcherViewModel;
bool _isSwitchingWallet = false;
bool get isSwitchingWallet => _isSwitchingWallet;
Future<AnyPayFormFill?> handleEvaluation(
BuildContext context,
AnyPayEvaluation evaluation, {
CryptoCurrency? fallbackCurrency,
}) async {
final request = evaluation.request;
if (!request.rawInput.toLowerCase().startsWith("ethereum:") &&
(request.rawInput.contains("@") || request.address.contains("@"))) {
return null;
}
try {
return await _handleDecision(context, evaluation, fallbackCurrency);
} catch (e) {
printV("Payment flow error: $e");
return AnyPayFormFill(request: request);
}
}
Future<AnyPayFormFill?> _handleDecision(
BuildContext context,
AnyPayEvaluation evaluation,
CryptoCurrency? fallbackCurrency,
) async {
final request = evaluation.request;
switch (evaluation.decision) {
case final AnyPayApplyToCurrentWallet decision:
if (request.isLightning) {
_enterLightningMode();
}
return AnyPayFormFill(
request: request,
token: decision.token,
fallbackCurrency: fallbackCurrency ?? decision.fallbackCurrency,
amountOverride: decision.amountOverride,
);
case AnyPayEvmNetworkChoice():
return _pickEvmNetwork(context, request);
case final AnyPayCrossChainPayment decision:
return _handleCrossChain(context, request, decision, fallbackCurrency);
case final AnyPayUnsupportedNetwork decision:
_showError(
context,
S.of(context).unsupported_network_requested(decision.chainId.toString()),
);
return null;
case AnyPayUnsupportedToken():
_showError(context, S.of(context).unsupported_token_requested);
return null;
case AnyPayEmptyInput():
return null;
}
}
Future<AnyPayFormFill?> _pickEvmNetwork(BuildContext context, AnyPayRequest request) async {
final chains = evm!.getAllChains();
final networks = chains.map((chain) {
final walletType = evm!.getWalletTypeByChainId(chain.chainId) ?? sendViewModel.wallet.type;
return RecipientNetworkItem(
chainId: chain.chainId,
name: chain.name,
iconPath: getCryptoCurrencyIconForWalletListItem(walletType, chainId: chain.chainId),
chainBadgeIconPath: symbolIconPathForWalletType(walletType),
);
}).toList();
final selectedChainId =
await EvmAddressDetectedSheet.show(context: context, networks: networks);
if (!context.mounted || selectedChainId == null) {
return null;
}
final target = chains.firstWhere(
(chain) => chain.chainId == selectedChainId,
orElse: () => chains.first,
);
final evaluation = await anyPayService.evaluateForEvmChain(request, target.chainId);
if (!context.mounted) {
return null;
}
return handleEvaluation(context, evaluation, fallbackCurrency: target.currency);
}
Future<AnyPayFormFill?> _handleCrossChain(
BuildContext context,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
CryptoCurrency? fallbackCurrency,
) async {
final destinationType = decision.targetWalletType;
final isEvmTarget = isEVMCompatibleChain(destinationType);
final destinationNetworkName = networkDisplayName(destinationType, decision.targetChainId);
final destinationNetworkIcon = symbolIconPathForWalletType(destinationType) ?? "";
if (!decision.canSwap && !decision.hasCompatibleWallet) {
_showError(
context,
S.of(context).swap_unavailable_needs_network_wallet(destinationNetworkName),
);
return null;
}
final currentType = sendViewModel.wallet.type;
final currentChainId = isEVMCompatibleChain(currentType) ? _currentEvmChainId() : null;
final currentNetworkName = networkDisplayName(currentType, currentChainId);
final currentNetworkIcon = symbolIconPathForWalletType(currentType) ?? "";
if (!decision.hasCompatibleWallet) {
final swapConfirmTitle = isEvmTarget
? S.of(context).swap_from_network(currentNetworkName)
: S.of(context).network_address_detected(destinationNetworkName);
final swapConfirmPrimary = isEvmTarget
? S.of(context).continue_to_swap
: S.of(context).swap_from_network(currentNetworkName);
await Navigator.of(context).push<void>(
CupertinoPageRoute(
builder: (pageContext) => NetworkDecisionPage(
title: swapConfirmTitle,
description: S.of(context).swap_from_network_description(
destinationNetworkName,
currentNetworkName,
),
destinationIconPath: destinationNetworkIcon,
currentIconPath: currentNetworkIcon,
primaryText: swapConfirmPrimary,
primaryIconPath: isEvmTarget ? null : "assets/new-ui/swap_arrows.svg",
onPrimary: () => _openSwap(pageContext, request, decision),
secondaryText: S.of(context).cancel,
),
),
);
return null;
}
final decisionTitle = isEvmTarget
? S.of(context).send_to_network(destinationNetworkName)
: S.of(context).network_address_detected(destinationNetworkName);
final selectedWallet = await Navigator.of(context).push<WalletInfo>(
CupertinoPageRoute(
builder: (pageContext) => NetworkDecisionPage(
title: decisionTitle,
description: decision.canSwap
? S.of(context).send_to_network_description(
destinationNetworkName,
currentNetworkName,
)
: S.of(context).swap_unavailable_switch_to_network(destinationNetworkName),
destinationIconPath: destinationNetworkIcon,
primaryText: S.of(context).switch_to_x_wallet(destinationNetworkName),
primaryIconPath: "assets/new-ui/wallet_filled.svg",
onPrimary: () => _pickDestinationWallet(pageContext, decision, destinationNetworkName),
secondaryText: decision.canSwap
? S.of(context).swap_from_network(currentNetworkName)
: S.of(context).cancel,
secondaryIconPath: decision.canSwap ? "assets/new-ui/swap_arrows.svg" : null,
onSecondary: decision.canSwap ? () => _openSwap(pageContext, request, decision) : null,
),
),
);
if (selectedWallet == null || !context.mounted) {
return null;
}
return _switchWallet(context, selectedWallet, request, decision, fallbackCurrency);
}
Future<void> _pickDestinationWallet(
BuildContext pageContext,
AnyPayCrossChainPayment decision,
String networkName,
) async {
WalletInfo? destinationWalletInfo;
if (decision.wallets.length > 1) {
destinationWalletInfo = await SwitchNetworkWalletPage.push(
context: pageContext,
networkName: networkName,
targetIconPath: symbolIconPathForWalletType(decision.targetWalletType) ?? "",
wallets: decision.wallets,
);
} else {
destinationWalletInfo = decision.wallets.first;
}
if (destinationWalletInfo == null || !pageContext.mounted) {
return;
}
Navigator.of(pageContext).pop(destinationWalletInfo);
}
Future<AnyPayFormFill?> _switchWallet(
BuildContext context,
WalletInfo walletInfo,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
CryptoCurrency? fallbackCurrency,
) async {
if (request.isLightning) {
_enterLightningMode();
}
_isSwitchingWallet = true;
BuildContext? loadingSheetContext;
bool completedFlow = false;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted || completedFlow) {
return;
}
showModalBottomSheet<void>(
context: context,
isDismissible: false,
builder: (sheetContext) {
if (completedFlow) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (Navigator.canPop(sheetContext)) {
Navigator.of(sheetContext).pop();
}
});
return const SizedBox.shrink();
}
loadingSheetContext = sheetContext;
return LoadingBottomSheet(titleText: S.of(sheetContext).loading_your_wallet);
},
);
});
bool success = false;
try {
success = await anyPayService.switchWalletForPayment(
walletInfo,
chainId: decision.targetChainId,
);
} finally {
_isSwitchingWallet = false;
completedFlow = true;
if (loadingSheetContext != null &&
loadingSheetContext!.mounted &&
Navigator.canPop(loadingSheetContext!)) {
Navigator.of(loadingSheetContext!).pop();
}
loadingSheetContext = null;
}
if (!context.mounted) {
return null;
}
if (!success) {
_showError(context, S.of(context).network_switch_failed);
return null;
}
return AnyPayFormFill(
request: request,
token: decision.token,
fallbackCurrency: fallbackCurrency ?? request.detection.detectedCurrency,
amountOverride: decision.amountOverride,
);
}
Future<void> _openSwap(
BuildContext presentContext,
AnyPayRequest request,
AnyPayCrossChainPayment decision,
) async {
if (!presentContext.mounted) {
return;
}
if (request.hasContract && decision.token == null) {
_showError(presentContext, S.of(presentContext).unsupported_token_requested);
return;
}
final intent = anyPayService.buildSwapIntent(
request,
targetWalletType: decision.targetWalletType,
targetChainId: decision.targetChainId,
token: decision.token,
);
final page = NewSwapPage(
getIt.get<ExchangeViewModel>(),
authService,
getIt.get<AddressResolverService>(),
null,
walletSwitcherViewModel: walletSwitcherViewModel,
fromSend: SwapFromSendArgs.fromIntent(intent),
balanceViewModel: sendViewModel.balanceViewModel,
);
await Navigator.of(presentContext).push<void>(
CupertinoPageRoute(
builder: (context) => Material(
color: Theme.of(context).colorScheme.surface,
child: SafeArea(bottom: false, child: page),
),
),
);
}
void _showError(BuildContext context, String message) {
if (!context.mounted) {
return;
}
showPopUp<void>(
context: context,
builder: (context) => AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: message,
buttonText: S.of(context).ok,
buttonAction: () => Navigator.of(context).pop(),
),
);
}
void _enterLightningMode() {
sendViewModel.selectedCryptoCurrency = CryptoCurrency.btcln;
sendViewModel.coinTypeToSpendFrom = UnspentCoinType.lightning;
}
int _currentEvmChainId() {
final wallet = sendViewModel.wallet;
return evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
}
}
@@ -5,31 +5,37 @@ import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:flutter/material.dart";
class SwapFromNetworkPage extends StatelessWidget {
const SwapFromNetworkPage({
class NetworkDecisionPage extends StatelessWidget {
const NetworkDecisionPage({
required this.title,
required this.primaryButtonText,
required this.destinationNetworkName,
required this.destinationNetworkIconPath,
required this.currentNetworkName,
required this.currentNetworkIconPath,
required this.onProceed,
required this.description,
required this.destinationIconPath,
required this.primaryText,
required this.onPrimary,
required this.secondaryText,
this.currentIconPath,
this.primaryIconPath,
this.secondaryIconPath,
this.onSecondary,
super.key,
this.primaryHasSwapIcon = false,
});
final String title;
final String primaryButtonText;
final bool primaryHasSwapIcon;
final String destinationNetworkName;
final String destinationNetworkIconPath;
final String currentNetworkName;
final String currentNetworkIconPath;
final VoidCallback onProceed;
final String description;
final String destinationIconPath;
final String? currentIconPath;
final String primaryText;
final VoidCallback onPrimary;
final String? primaryIconPath;
final String secondaryText;
final String? secondaryIconPath;
final VoidCallback? onSecondary;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final primaryIcon = primaryIconPath;
final secondaryIcon = secondaryIconPath;
return Material(
color: colors.surface,
child: SafeArea(
@@ -40,9 +46,6 @@ class SwapFromNetworkPage extends StatelessWidget {
leadingIcon: const Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).maybePop(),
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
child: Center(
@@ -51,30 +54,11 @@ class SwapFromNetworkPage extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
CakeImageWidget(
imageUrl: currentNetworkIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(currentNetworkIconPath)
? colors.primary
: null,
),
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
CakeImageWidget(
imageUrl: destinationNetworkIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationNetworkIconPath)
? colors.primary
: null,
),
],
ExcludeSemantics(
child: _DecisionHeader(
destinationIconPath: destinationIconPath,
currentIconPath: currentIconPath,
),
),
const SizedBox(height: 24),
Text(
@@ -88,10 +72,7 @@ class SwapFromNetworkPage extends StatelessWidget {
),
const SizedBox(height: 16),
Text(
S.of(context).swap_from_network_description(
destinationNetworkName,
currentNetworkName,
),
description,
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
@@ -109,23 +90,31 @@ class SwapFromNetworkPage extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
NewPrimaryButton(
onPressed: onProceed,
image: primaryHasSwapIcon
onPressed: onPrimary,
image: primaryIcon != null
? CakeImageWidget(
imageUrl: "assets/new-ui/swap_arrows.svg",
imageUrl: primaryIcon,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
)
: null,
text: primaryButtonText,
text: primaryText,
color: colors.primary,
textColor: colors.onPrimary,
),
const SizedBox(height: 12),
NewPrimaryButton(
onPressed: () => Navigator.of(context).maybePop(),
text: S.of(context).cancel,
onPressed: onSecondary ?? () => Navigator.of(context).maybePop(),
image: secondaryIcon != null
? CakeImageWidget(
imageUrl: secondaryIcon,
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
)
: null,
text: secondaryText,
color: colors.surfaceContainer,
textColor: colors.primary,
),
@@ -138,3 +127,48 @@ class SwapFromNetworkPage extends StatelessWidget {
);
}
}
class _DecisionHeader extends StatelessWidget {
const _DecisionHeader({required this.destinationIconPath, this.currentIconPath});
final String destinationIconPath;
final String? currentIconPath;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final current = currentIconPath;
if (current == null) {
return CakeImageWidget(
imageUrl: destinationIconPath,
width: 75,
height: 75,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
);
}
return Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
CakeImageWidget(
imageUrl: current,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(current) ? colors.primary : null,
),
Icon(Icons.arrow_forward, color: colors.primary, size: 28),
CakeImageWidget(
imageUrl: destinationIconPath,
width: 50,
height: 50,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
),
],
);
}
}
@@ -1,46 +1,14 @@
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_base.dart";
import "package:flutter/material.dart";
class RecipientNetworkSelector extends StatelessWidget {
const RecipientNetworkSelector({
super.key,
required this.wallet,
required this.onTap,
});
final WalletBase wallet;
final void Function(ChainInfo currentChain) onTap;
@override
Widget build(BuildContext context) {
if (evm == null) return const SizedBox.shrink();
final currentChainId =
evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type);
final currentChain = evm!.getChainInfoByChainId(currentChainId);
if (currentChain == null) {
printV("RecipientNetworkSelector: no chain info for chainId $currentChainId");
return const SizedBox.shrink();
}
return _RecipientNetworkRow(
networkName: currentChain.name,
networkIconPath: symbolIconPathForWalletType(wallet.type) ?? "",
onTap: () => onTap(currentChain),
);
}
}
class _RecipientNetworkRow extends StatelessWidget {
const _RecipientNetworkRow({
required this.networkName,
required this.networkIconPath,
required this.onTap,
super.key,
});
final String networkName;
@@ -50,50 +18,56 @@ class _RecipientNetworkRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).network_prefix_on,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
const SizedBox(width: 12),
CakeImageWidget(
imageUrl: networkIconPath,
width: 16,
height: 16,
fit: BoxFit.cover,
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
),
const SizedBox(width: 8),
Text(
networkName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
letterSpacing: -0.07,
color: colors.primary,
return Semantics(
button: true,
label: "${S.of(context).network_prefix_on} $networkName",
child: GestureDetector(
onTap: onTap,
child: ExcludeSemantics(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).network_prefix_on,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
const SizedBox(width: 12),
CakeImageWidget(
imageUrl: networkIconPath,
width: 16,
height: 16,
fit: BoxFit.cover,
color: isMonochromeSymbolIcon(networkIconPath) ? colors.primary : null,
),
const SizedBox(width: 8),
Text(
networkName,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w400,
letterSpacing: -0.07,
color: colors.primary,
),
),
const SizedBox(width: 8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: colors.surfaceContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.keyboard_arrow_down,
size: 18,
color: colors.onSurfaceVariant,
),
),
],
),
const SizedBox(width: 8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: colors.surfaceContainer,
shape: BoxShape.circle,
),
child: Icon(
Icons.keyboard_arrow_down,
size: 18,
color: colors.onSurfaceVariant,
),
),
],
),
),
);
}
@@ -1,122 +0,0 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
import "package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:flutter/material.dart";
class SendToNetworkPage extends StatelessWidget {
const SendToNetworkPage({
required this.title,
required this.destinationNetworkName,
required this.destinationIconPath,
required this.currentNetworkName,
required this.onSwitchWallet,
required this.onSwap,
super.key,
});
final String title;
final String destinationNetworkName;
final String destinationIconPath;
final String currentNetworkName;
final VoidCallback onSwitchWallet;
final VoidCallback onSwap;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Material(
color: colors.surface,
child: SafeArea(
child: Column(
children: [
ModalTopBar(
title: "",
leadingIcon: const Icon(Icons.arrow_back_ios_new),
leadingSemanticLabel: S.of(context).seed_alert_back,
onLeadingPressed: () => Navigator.of(context).maybePop(),
trailingIcon: const Icon(Icons.close),
trailingSemanticLabel: S.of(context).close,
onTrailingPressed: () => Navigator.of(context).maybePop(),
),
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: destinationIconPath,
width: 75,
height: 75,
fit: BoxFit.contain,
color: isMonochromeSymbolIcon(destinationIconPath) ? colors.primary : null,
),
const SizedBox(height: 24),
Text(
title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontSize: 20,
fontWeight: FontWeight.w500,
letterSpacing: -0.1,
),
),
const SizedBox(height: 16),
Text(
S.of(context).send_to_network_description(
destinationNetworkName,
currentNetworkName,
),
textAlign: TextAlign.center,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: colors.onSurfaceVariant, letterSpacing: -0.07),
),
],
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
NewPrimaryButton(
onPressed: onSwitchWallet,
image: CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.onPrimary, BlendMode.srcIn),
),
text: S.of(context).switch_to_x_wallet(destinationNetworkName),
color: colors.primary,
textColor: colors.onPrimary,
),
const SizedBox(height: 12),
NewPrimaryButton(
onPressed: onSwap,
image: CakeImageWidget(
imageUrl: "assets/new-ui/swap_arrows.svg",
width: 24,
height: 24,
colorFilter: ColorFilter.mode(colors.primary, BlendMode.srcIn),
),
text: S.of(context).swap_from_network(currentNetworkName),
color: colors.surfaceContainer,
textColor: colors.primary,
),
],
),
),
],
),
),
);
}
}
+18 -13
View File
@@ -181,23 +181,28 @@ class ChangelogItemWidget extends StatelessWidget {
width: 36,
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
if (item.title.isNotEmpty)
Text(
item.title,
style: TextStyle(fontWeight: FontWeight.w500),
),
if (item.description.isNotEmpty)
Text(item.description,
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 4,
children: [
if (item.title.isNotEmpty)
Text(
item.title,
style: TextStyle(fontWeight: FontWeight.w500),
),
if (item.description.isNotEmpty)
Text(
item.description,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
))
],
),
)
],
),
)
],
);
}
}
@@ -162,7 +162,8 @@ class CoinActionRow extends StatelessWidget {
if (code == null || code.isEmpty) return;
late final PaymentRequest req;
PaymentRequest? req;
String? rawInput;
var unspentCoinType = UnspentCoinType.any;
if (SendViewModelBase.isNonZeroAmountLightningInvoice(code)) {
unspentCoinType = UnspentCoinType.lightning;
@@ -172,8 +173,6 @@ class CoinActionRow extends StatelessWidget {
unspentCoinType = UnspentCoinType.lightning;
final amount = (await LNURL.getPayRequestAmount(code))?.toString() ?? "0";
req = PaymentRequest(code, amount, "", "", "");
} else if (OpenCryptoPayService.isOpenCryptoPayQR(code)) {
req = PaymentRequest(code, "", "", "", "");
} else if (Uri.tryParse(code)?.scheme == "wc") {
if (!isWalletConnectCompatibleChain(walletType)) {
showPopUp<void>(
@@ -190,19 +189,19 @@ class CoinActionRow extends StatelessWidget {
Navigator.of(context)
.pushNamed(Routes.walletConnectConnectionsListing, arguments: Uri.parse(code));
return;
} else if (["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
} else if (!OpenCryptoPayService.isOpenCryptoPayQR(code) &&
["http", "https", "tcp"].contains(Uri.tryParse(code)?.scheme)) {
Navigator.of(context).pushNamed(Routes.newNode,
arguments: {"editingNode": Node.fromUri(Uri.parse(code), walletType)});
return;
} else {
final uri = Uri.tryParse(code);
if (uri == null) return;
req = PaymentRequest.fromUri(uri);
rawInput = code;
}
final sendPage = getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: req,
initialRawInput: rawInput,
unspentCoinType: unspentCoinType,
),
);
@@ -10,18 +10,20 @@ class ChainIcon extends StatelessWidget {
{super.key,
required this.iconPath,
required this.dashboardViewModel,
required this.isSyncHeavy});
required this.isSyncHeavy,
required this.showSyncedMessage});
final String iconPath;
final bool isSyncHeavy;
final DashboardViewModel dashboardViewModel;
final bool showSyncedMessage;
@override
Widget build(BuildContext context) {
return Observer(
builder: (_) {
final progress = dashboardViewModel.status.progress();
final done = !isSyncHeavy || progress >= 1;
final done = !showSyncedMessage && (!isSyncHeavy || progress >= 1);
return Stack(
children: [
@@ -30,10 +32,10 @@ class ChainIcon extends StatelessWidget {
opacity: done ? 0 : 1,
// Faded out means "nothing to report", so it must leave the tree too.
child: ExcludeSemantics(
excluding: done,
excluding: done || showSyncedMessage,
child: CircularProgressIndicator(
value: progress,
color: Color(0xFFFFB84E),
color: showSyncedMessage ? Color(0xFFFF12A439) : Color(0xFFFFB84E),
strokeWidth: 2,
semanticsLabel: S.of(context).synchronizing,
semanticsValue: "${(progress * 100).round()}%",
@@ -1,21 +1,26 @@
import 'package:cake_wallet/core/sync_status_title.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/pulsing_dot.dart';
import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
import "package:flutter/cupertino.dart";
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class SyncBar extends StatelessWidget {
SyncBar({super.key, required this.dashboardViewModel, required this.isSyncHeavy});
SyncBar({
super.key,
required this.dashboardViewModel,
required this.isSyncHeavy,
required this.showSyncedMessage,
});
final DashboardViewModel dashboardViewModel;
final bool isSyncHeavy;
final bool showSyncedMessage;
static const failStatuses = [
FailedSyncStatus,
@@ -37,9 +42,10 @@ class SyncBar extends StatelessWidget {
ConnectedSyncStatus,
];
static const syncedColor = Color(0xFF12A439);
@override
Widget build(BuildContext context) {
return Observer(
Widget build(BuildContext context) => Observer(
builder: (_) {
final status = dashboardViewModel.status;
final Widget? icon = _getIcon(context, status.runtimeType);
@@ -106,7 +112,6 @@ class SyncBar extends StatelessWidget {
);
},
);
}
void _openNodeManagement(BuildContext context) =>
CupertinoScaffold.showCupertinoModalBottomSheet(
@@ -130,8 +135,9 @@ class SyncBar extends StatelessWidget {
width: 20,
height: 20,
),
if (_showDot()) PulsingDot(),
],
if (_showDot()) const CupertinoActivityIndicator(radius: 8),
if(_showLightSyncCheck()) const Icon(Icons.check, color: syncedColor, size: 18),
],
);
final label = _joinLabels([
@@ -144,6 +150,9 @@ class SyncBar extends StatelessWidget {
return Semantics(label: label, child: ExcludeSemantics(child: row));
}
bool get _isShowingSyncedMessage =>
showSyncedMessage && dashboardViewModel.status.runtimeType == SyncedSyncStatus;
String _statusSemanticsLabel(BuildContext context, SyncStatus status) {
final isFailure = failStatuses.contains(status.runtimeType);
@@ -166,7 +175,7 @@ class SyncBar extends StatelessWidget {
}
Border? _getBorder(BuildContext context, Type status) {
if (progressStatuses.contains(status)) {
if (progressStatuses.contains(status) || _isShowingSyncedMessage) {
return Border.all(color: Theme.of(context).colorScheme.surfaceContainerHigh, width: 1);
}
@@ -174,19 +183,26 @@ class SyncBar extends StatelessWidget {
}
TextStyle? _getTextStyle(BuildContext context, Type status) {
final Color color;
if (failStatuses.contains(status)) {
return TextStyle(
fontSize: 12, fontWeight: FontWeight.w400, color: Theme.of(context).colorScheme.error);
color = Theme.of(context).colorScheme.error;
} else if (status == SyncedSyncStatus) {
color = syncedColor;
} else {
return TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Theme.of(context).colorScheme.onSurfaceVariant);
color = Theme.of(context).colorScheme.onSurfaceVariant;
}
return TextStyle(
fontSize: 12, fontWeight: FontWeight.w400, color: color);
}
Widget? _getIcon(BuildContext context, Type status) {
if (status == LostConnectionSyncStatus || status == FailedSyncStatus) {
if(status == SyncedSyncStatus) {
return Icon(Icons.check, color: syncedColor, size: 12);
}
if (status == LostConnectionSyncStatus) {
return CakeImageWidget(
imageUrl: "assets/new-ui/offline.svg",
colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.error, BlendMode.srcIn),
@@ -228,11 +244,14 @@ class SyncBar extends StatelessWidget {
}
bool _showFullBar() {
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus) return false;
if (dashboardViewModel.status.runtimeType == SyncedSyncStatus)
return isSyncHeavy && _isShowingSyncedMessage;
return isSyncHeavy || failStatuses.contains(dashboardViewModel.status.runtimeType);
}
bool _showDot() {
return !isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
}
bool _showDot() =>
!isSyncHeavy && progressStatuses.contains(dashboardViewModel.status.runtimeType);
bool _showLightSyncCheck() =>
!isSyncHeavy && _isShowingSyncedMessage;
}
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/generated/i18n.dart';
@@ -6,11 +7,13 @@ import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/lightning_s
import 'package:cake_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/sync_status.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class TopBar extends StatelessWidget {
class TopBar extends StatefulWidget {
const TopBar({
super.key,
required this.lightningMode,
@@ -25,33 +28,76 @@ class TopBar extends StatelessWidget {
final DashboardViewModel dashboardViewModel;
@override
Widget build(BuildContext context) {
return Padding(
State<TopBar> createState() => _TopBarState();
}
class _TopBarState extends State<TopBar> {
static const syncedMessageDuration = Duration(seconds: 3);
bool showSyncedMessage = false;
Timer? syncedMessageTimer;
late final ReactionDisposer? _statusReactionDisposer;
@override
void initState() {
super.initState();
_statusReactionDisposer = reaction(
(_) => widget.dashboardViewModel.status.runtimeType,
(status) {
syncedMessageTimer?.cancel();
if (status == SyncedSyncStatus) {
if(mounted) {
setState(() => showSyncedMessage = true);
}
syncedMessageTimer =
Timer(syncedMessageDuration, () => setState(() => showSyncedMessage = false));
} else {
if(mounted) {
setState(() => showSyncedMessage = false);
}
}
},
);
}
@override
void dispose() {
syncedMessageTimer?.cancel();
_statusReactionDisposer?.reaction.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.only(
bottom: 10, left: 18, right: 18, top: 10 + _additionalTopPadding(context)),
child: Observer(
builder: (_) => Row(
spacing: 12,
children: [
(dashboardViewModel.hasLightning)
(widget.dashboardViewModel.hasLightning)
? LightningSwitcher(
lightningMode: lightningMode,
onLightningSwitchPress: onLightningSwitchPress,
lightningMode: widget.lightningMode,
onLightningSwitchPress: widget.onLightningSwitchPress,
)
: ChainIcon(
iconPath: dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: dashboardViewModel,
isSyncHeavy: dashboardViewModel.isSyncHeavy),
iconPath: widget.dashboardViewModel.wallet.currency.flatIconPath ?? "",
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage),
SyncBar(
dashboardViewModel: dashboardViewModel,
isSyncHeavy: dashboardViewModel.isSyncHeavy,
dashboardViewModel: widget.dashboardViewModel,
isSyncHeavy: widget.dashboardViewModel.isSyncHeavy,
showSyncedMessage: showSyncedMessage,
),
ModernButton.svg(
iconColor: Theme.of(context).colorScheme.primary,
size: 36,
onPressed: () {
HapticFeedback.mediumImpact();
onSettingsButtonPress();
widget.onSettingsButtonPress();
},
svgPath: "assets/new-ui/top-settings.svg",
semanticLabel: S.of(context).settings_title,
@@ -60,7 +106,6 @@ class TopBar extends StatelessWidget {
),
),
);
}
//FIXME remove after this gets fixed flutter-side
double _additionalTopPadding(BuildContext context) {
@@ -18,6 +18,22 @@ class AnyPaySwapFooter extends StatelessWidget {
final textTheme = Theme.of(context).textTheme;
return Observer(
builder: (_) {
if (exchangeViewModel.noProviderForPair) {
return Container(
alignment: Alignment.center,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(18),
border: Border.all(color: colors.error),
),
child: Text(
S.of(context).no_providers_available,
style: textTheme.bodyMedium?.copyWith(color: colors.error, letterSpacing: -0.07),
),
);
}
if (exchangeViewModel.depositAmount.isEmpty) {
return const SizedBox.shrink();
}
+157 -205
View File
@@ -8,6 +8,7 @@ import "package:cake_wallet/new-ui/widgets/send_page/fiat_amount_bar.dart";
import "package:cake_wallet/new-ui/widgets/send_page/send_memo_input.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/refund_address_modal.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_modal.dart";
import "package:cake_wallet/new-ui/widgets/swap_page/swap_source_selector.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:cake_wallet/utils/decimal_input_formatter.dart";
import "package:cake_wallet/utils/permission_handler.dart";
@@ -82,10 +83,14 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
final fiatAmountController = TextEditingController();
final amountFocusNode = FocusNode();
final memoController = TextEditingController();
final _amountFieldKey = GlobalKey<FormFieldState<String>>();
mobx.ReactionDisposer? _memoReactionDisposer;
@override
void initState() {
amountController
.addListener(() => _amountFieldKey.currentState?.didChange(amountController.text));
if (widget.isReceiverCard) {
memoController.text = widget.exchangeViewModel.receiveAddressExtraId;
@@ -145,95 +150,13 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
final colors = Theme.of(context).colorScheme;
if (widget.sourceSelectorMode) {
final available = _sourceBalance()?.amount.split(" ").first ?? "—";
return Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 24,
height: 24,
),
const SizedBox(width: 8),
Text(
currencyToShow,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
const Spacer(),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 16,
height: 16,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
const SizedBox(width: 4),
Flexible(
child: Text(
widget.walletName ?? "",
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.onSurfaceVariant,
),
),
),
],
),
),
const SizedBox(width: 8),
Text(
"${S.of(context).avl}: $available",
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.primary,
),
),
],
),
),
],
return SwapSourceSelector(
currencyIconPath: widget.currency.iconPath ?? "",
currencyLabel: currencyToShow,
chainIconPath: chainIconPath,
walletName: widget.walletName,
availableBalance: _sourceBalance()?.amount.split(" ").first ?? "—",
onTap: _presentCurrencyPicker,
);
}
@@ -260,139 +183,168 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
child: Column(
spacing: 12,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: amountFocusNode.requestFocus,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 4,
FormField<String>(
key: _amountFieldKey,
initialValue: amountController.text,
validator: _fiatInputMode ? null : widget.currencyValueValidator,
builder: (state) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: amountFocusNode.requestFocus,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: IntrinsicWidth(
child: Observer(
builder: (_) {
final showFetching = !widget.isReceiverCard &&
widget.exchangeViewModel.isFixedRateMode &&
widget.exchangeViewModel.receiveAmount.isNotEmpty &&
widget.exchangeViewModel.depositAmount.isEmpty;
return TextFormField(
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: !widget.useBaseUnit,
Expanded(
child: Row(
spacing: 4,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: IntrinsicWidth(
child: Observer(
builder: (_) {
final showFetching = !widget.isReceiverCard &&
widget.exchangeViewModel.isFixedRateMode &&
widget.exchangeViewModel.receiveAmount.isNotEmpty &&
widget.exchangeViewModel.depositAmount.isEmpty;
return TextField(
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: !widget.useBaseUnit,
),
onChanged: state.didChange,
controller: _fiatInputMode
? fiatAmountController
: amountController,
focusNode: amountFocusNode,
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
isDense: true,
hintText: showFetching ? S.of(context).fetching : "0",
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
inputFormatters: <TextInputFormatter>[
DecimalInputFormatter(
maxDecimals: widget.useBaseUnit
? 0
: widget.currency.decimals,
),
],
);
},
),
validator:
_fiatInputMode ? null : widget.currencyValueValidator,
controller:
_fiatInputMode ? fiatAmountController : amountController,
focusNode: amountFocusNode,
style: TextStyle(
fontSize: 28,
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
contentPadding: EdgeInsets.zero,
isDense: true,
hintText: showFetching ? S.of(context).fetching : "0",
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
),
inputFormatters: <TextInputFormatter>[
DecimalInputFormatter(
maxDecimals:
widget.useBaseUnit ? 0 : widget.currency.decimals,
),
),
if (_fiatInputMode)
Center(
child: Text(
widget.exchangeViewModel.fiat.title,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
],
);
},
),
),
),
],
),
),
if (_fiatInputMode)
Center(
child: Text(
widget.exchangeViewModel.fiat.title,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant,
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(999999),
),
child: Padding(
padding:
const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 28,
height: 28,
),
const SizedBox(width: 10),
Text(
currencyToShow,
textAlign: TextAlign.center,
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
const SizedBox(width: 6),
] else
const SizedBox(width: 10),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999999999),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
width: 4,
height: 4,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
),
),
const SizedBox(width: 4),
],
),
),
),
),
],
),
),
GestureDetector(
onTap: _presentCurrencyPicker,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(999999),
),
child: Padding(
padding: const EdgeInsets.only(top: 4, bottom: 4, left: 4, right: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CakeImageWidget(
imageUrl: widget.currency.iconPath ?? "",
width: 28,
height: 28,
),
const SizedBox(width: 10),
Text(
currencyToShow,
textAlign: TextAlign.center,
),
if (chainIconPath != null && chainIconPath.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIconPath,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
),
const SizedBox(width: 6),
] else
const SizedBox(width: 10),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(9999999999),
color: Theme.of(context).colorScheme.surfaceContainerHigh,
),
child: Padding(
padding: const EdgeInsets.all(4),
child: RotatedBox(
quarterTurns: 2,
child: CakeImageWidget(
imageUrl: "assets/new-ui/dropdown_arrow.svg",
width: 4,
height: 4,
colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary,
BlendMode.srcIn,
),
),
),
),
),
const SizedBox(width: 4),
],
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Semantics(
container: true,
liveRegion: true,
label: "${S.of(context).amount}${state.errorText!}",
excludeSemantics: true,
child: Text(
state.errorText!,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.error,
),
),
),
),
),
],
),
),
@@ -1,4 +1,4 @@
import "package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
@@ -8,13 +8,18 @@ class SwapFromSendArgs {
required this.recipientAddress,
required this.receiveCurrency,
required this.targetWalletType,
this.depositBalanceByAsset,
this.receiveAmount,
});
factory SwapFromSendArgs.fromIntent(AnyPaySwapIntent intent) => SwapFromSendArgs(
recipientAddress: intent.recipientAddress,
receiveCurrency: intent.receiveCurrency,
targetWalletType: intent.targetWalletType,
receiveAmount: intent.receiveAmount,
);
final String recipientAddress;
final CryptoCurrency receiveCurrency;
final WalletType targetWalletType;
final Map<CryptoCurrency, CurrencyPickerBalance>? depositBalanceByAsset;
final Money? receiveAmount;
}
@@ -0,0 +1,118 @@
import "package:cake_wallet/generated/i18n.dart";
import "package:cake_wallet/src/widgets/cake_image_widget.dart";
import "package:flutter/material.dart";
class SwapSourceSelector extends StatelessWidget {
const SwapSourceSelector({
required this.currencyIconPath,
required this.currencyLabel,
required this.availableBalance,
required this.onTap,
this.chainIconPath,
this.walletName,
super.key,
});
final String currencyIconPath;
final String currencyLabel;
final String availableBalance;
final VoidCallback onTap;
final String? chainIconPath;
final String? walletName;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final chainIcon = chainIconPath;
return Column(
spacing: 12,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: onTap,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(18),
),
child: Row(
children: [
CakeImageWidget(
imageUrl: currencyIconPath,
width: 24,
height: 24,
),
const SizedBox(width: 8),
Text(
currencyLabel,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(letterSpacing: -0.08),
),
if (chainIcon != null && chainIcon.isNotEmpty) ...[
const SizedBox(width: 4),
CakeImageWidget(
imageUrl: chainIcon,
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
const Spacer(),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
width: 12,
height: 12,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CakeImageWidget(
imageUrl: "assets/new-ui/wallet_filled.svg",
width: 16,
height: 16,
colorFilter: ColorFilter.mode(colors.onSurfaceVariant, BlendMode.srcIn),
),
const SizedBox(width: 4),
Flexible(
child: Text(
walletName ?? "",
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.onSurfaceVariant,
),
),
),
],
),
),
const SizedBox(width: 8),
Text(
S.of(context).available_balance_short(availableBalance),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.06,
color: colors.primary,
),
),
],
),
),
],
);
}
}