CW1612: AnyPay Service Layer (#3516)

* fix android CI

* feat: add anypay core models, parser and routing engine

* feat: add anypay resolver, anypay service and wallet switch service

* refactor: route the send page payment flow through anypay service

* fix: show message when there's no swap providers for a pair and reset the trade guard on dismissal

* refactor: merge the swap from network and send to network decision pages together and clean up anypay widgets

* feat: reapply deep links to a currently open send flow and register the solana scheme

* feat: add support for zcash address detection to anypay and other minor fixes

* refactor: remove trailing icon

* feat: hide swap flow for sp and mweb addresses

* feat: enhance zcash address detection and deeplink handling

* fix: pin bitcoin_base

* refactor: Move the anypay decision navigation out of the send page and address minor review comments

* refactor: Move the anypay decision navigation out of the send page and address minor review comments

* feat: handle exception in switch service and mvoe balance compute to call site

* refactor: combine anypay entry points to one and extract selected mode widget

---------

Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
This commit is contained in:
David Adegoke
2026-08-20 09:35:44 +02:00
committed by GitHub
co-authored by Czarek Nakamoto
parent 4cc1495686
commit 3b100eae33
67 changed files with 2672 additions and 1210 deletions
+4 -14
View File
@@ -177,13 +177,7 @@ class AddressValidator extends TextValidator {
if (ZcashNetworkType.isDevNetwork(network)) {
return null;
}
pattern = '(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
'|zs[a-z0-9]{76}'
'|zxviews[a-z0-9]{278}'
'|u1[a-z0-9]{1,300}'
')';
pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
case CryptoCurrency.dcr:
pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+';
case CryptoCurrency.rvn:
@@ -355,6 +349,8 @@ class AddressValidator extends TextValidator {
static String get silentPaymentAddressPatternMainnet => 'sp1[0-9a-zA-Z]{113}';
static String get silentPaymentAddressPatternTestnet => '(tsp|sprt)1[0-9a-zA-Z]{113}';
static String get mWebAddressPattern => MwebAddress.regex.pattern;
static String get zcashAddressPattern =>
"t1[0-9A-Za-z]{33}|t3[0-9A-Za-z]{33}|zs[a-z0-9]{76}|u1[a-z0-9]{1,300}";
static const String bolt11InvoiceMatcher = r'^(lightning:)?(lnbc|lntb|lnbs|lnbcrt)[a-z0-9]+$';
static const String bolt12OfferMatcher = r'^(lightning:)?(lno1)[a-z0-9]+$';
@@ -401,13 +397,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.zano:
pattern = '([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)';
case CryptoCurrency.zec:
pattern = '(?:'
't1[0-9A-Za-z]{33}'
'|t3[0-9A-Za-z]{33}'
'|zs[a-z0-9]{76}'
'|zxviews[a-z0-9]{278}'
'|u1[a-z0-9]{1,300}'
')';
pattern = "(?:$zcashAddressPattern|zxviews[a-z0-9]{278})";
default:
if (type.tag == CryptoCurrency.eth.title) {
pattern = '0x[0-9a-zA-Z]+';
+174
View File
@@ -0,0 +1,174 @@
import "package:cake_wallet/core/universal_address_detector.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
sealed class ChainBinding {
const ChainBinding();
}
class ExplicitEvmChain extends ChainBinding {
const ExplicitEvmChain(this.chainId);
final int chainId;
}
class ChainlessEvm extends ChainBinding {
const ChainlessEvm();
}
class NoEvmBinding extends ChainBinding {
const NoEvmBinding();
}
class AnyPayRequest {
AnyPayRequest({
required this.rawInput,
required this.paymentRequest,
required this.detection,
required this.chainBinding,
});
final String rawInput;
final PaymentRequest paymentRequest;
final AddressDetectionResult detection;
final ChainBinding chainBinding;
String get address => paymentRequest.address;
String get amount => paymentRequest.amount;
String? get contractAddress => paymentRequest.contractAddress;
bool get hasContract => contractAddress != null && contractAddress!.isNotEmpty;
bool get isLightning => detection.detectedCurrency == CryptoCurrency.btcln;
}
class WalletSnapshot {
WalletSnapshot({
required this.type,
required this.currentWalletIsEvm,
required this.currentChainId,
required this.wallets,
required this.supportedEvmChains,
required this.hasEvmProxy,
required this.hasSolanaProxy,
required this.hasTronProxy,
});
final WalletType type;
final bool currentWalletIsEvm;
final int? currentChainId;
final List<WalletInfo> wallets;
final Map<int, WalletType> supportedEvmChains;
final bool hasEvmProxy;
final bool hasSolanaProxy;
final bool hasTronProxy;
List<WalletInfo> walletsOfType(WalletType walletType) =>
wallets.where((wallet) => wallet.type == walletType).toList();
}
sealed class AnyPayResolution {
const AnyPayResolution();
}
class NoTokenRequested extends AnyPayResolution {
const NoTokenRequested();
}
class TokenResolved extends AnyPayResolution {
const TokenResolved({required this.token, this.chainId, this.amountOverride});
final CryptoCurrency token;
final int? chainId;
final String? amountOverride;
}
class TokenUnknown extends AnyPayResolution {
const TokenUnknown(this.contract);
final String contract;
}
sealed class AnyPayDecision {
const AnyPayDecision();
}
class AnyPayApplyToCurrentWallet extends AnyPayDecision {
const AnyPayApplyToCurrentWallet({
this.token,
this.amountOverride,
this.fallbackCurrency,
});
final CryptoCurrency? token;
final String? amountOverride;
final CryptoCurrency? fallbackCurrency;
}
class AnyPayEvmNetworkChoice extends AnyPayDecision {
const AnyPayEvmNetworkChoice();
}
class AnyPayCrossChainPayment extends AnyPayDecision {
const AnyPayCrossChainPayment({
required this.targetWalletType,
required this.wallets,
this.targetChainId,
this.token,
this.amountOverride,
this.canSwap = true,
});
final WalletType targetWalletType;
final int? targetChainId;
final List<WalletInfo> wallets;
final CryptoCurrency? token;
final String? amountOverride;
final bool canSwap;
bool get hasCompatibleWallet => wallets.isNotEmpty;
}
class AnyPayUnsupportedNetwork extends AnyPayDecision {
const AnyPayUnsupportedNetwork(this.chainId);
final int chainId;
}
class AnyPayUnsupportedToken extends AnyPayDecision {
const AnyPayUnsupportedToken(this.contract);
final String contract;
}
class AnyPayEmptyInput extends AnyPayDecision {
const AnyPayEmptyInput();
}
class AnyPayEvaluation {
AnyPayEvaluation({required this.request, required this.decision});
final AnyPayRequest request;
final AnyPayDecision decision;
}
class AnyPaySwapIntent {
AnyPaySwapIntent({
required this.request,
required this.receiveCurrency,
required this.targetWalletType,
this.receiveAmount,
});
final AnyPayRequest request;
final CryptoCurrency receiveCurrency;
final WalletType targetWalletType;
final Money? receiveAmount;
String get recipientAddress => request.address;
}
+71
View File
@@ -0,0 +1,71 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/universal_address_detector.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/payment_uris.dart";
class AnyPayParser {
static AnyPayRequest fromRaw(String input) {
final paymentRequest = PaymentRequest.fromString(input);
return AnyPayRequest(
rawInput: input,
paymentRequest: paymentRequest,
detection: UniversalAddressDetector.detectAddress(input),
chainBinding: _bindingFor(paymentRequest),
);
}
static AnyPayRequest fromPaymentRequest(PaymentRequest request) {
final raw = _reconstructRawInput(request);
return AnyPayRequest(
rawInput: raw,
paymentRequest: request,
detection: UniversalAddressDetector.detectAddress(raw),
chainBinding: _bindingFor(request),
);
}
static String _reconstructRawInput(PaymentRequest request) {
final scheme = request.scheme.toLowerCase();
if (scheme.isEmpty || scheme == "lightning") {
return request.address;
}
if (scheme == "ethereum") {
return ERC681URI(
address: request.address,
amount: request.amount,
contractAddress: request.contractAddress,
chainId: request.chainId ?? 1,
rawTokenAmount: request.rawTokenAmount,
).toString();
}
final amount = request.amount.isNotEmpty ? "?amount=${request.amount}" : "";
return "${request.scheme}:${request.address}$amount";
}
static ChainBinding _bindingFor(PaymentRequest request) {
final scheme = request.scheme.toLowerCase();
if (scheme == "ethereum") {
final chainId = request.chainId;
return chainId != null ? ExplicitEvmChain(chainId) : const ChainlessEvm();
}
if (scheme.isNotEmpty) {
try {
final chainId = getChainIdByCryptoCurrency(CryptoCurrency.fromString(scheme));
if (chainId != null) {
return ExplicitEvmChain(chainId);
}
} catch (_) {}
}
return const NoEvmBinding();
}
}
+109
View File
@@ -0,0 +1,109 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_router.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/utils/token_utilities.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayResolver {
AnyPayResolver({AnyPayTokenLookup? tokenLookup})
: _tokenLookup = tokenLookup ?? const TokenUtilitiesLookup();
final AnyPayTokenLookup _tokenLookup;
Future<AnyPayResolution> resolve(AnyPayRequest request, WalletSnapshot snapshot) async {
if (!request.hasContract) {
return const NoTokenRequested();
}
final contract = request.contractAddress!;
final detectedType = request.detection.detectedWalletType;
if (detectedType != null && isEVMCompatibleChain(detectedType)) {
return _resolveEvm(request, snapshot, contract);
}
final token = await _tokenLookup.findTokenByAddress(
walletType: detectedType ?? snapshot.type,
address: contract,
);
if (token == null) {
return TokenUnknown(contract);
}
return TokenResolved(
token: token,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
Future<AnyPayResolution> _resolveEvm(
AnyPayRequest request,
WalletSnapshot snapshot,
String contract,
) async {
final requestedChainId = AnyPayRouter.requestedEvmChainId(request, snapshot) ?? 1;
final lookupType = snapshot.supportedEvmChains[requestedChainId];
if (lookupType != null) {
final token =
await _tokenLookup.findTokenByAddress(walletType: lookupType, address: contract);
if (token != null) {
return TokenResolved(
token: token,
chainId: requestedChainId,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
}
// The QRs from our old app versions omit the chainId on mainnet, so a contract the target
// network does not know may still belong to another EVM network.
if (request.chainBinding is ChainlessEvm) {
final reboundChainId = await _tokenLookup.findEvmChainIdForContract(
contract,
excludingChainId: requestedChainId,
);
final reboundType =
reboundChainId != null ? snapshot.supportedEvmChains[reboundChainId] : null;
if (reboundType != null) {
final token =
await _tokenLookup.findTokenByAddress(walletType: reboundType, address: contract);
if (token != null) {
return TokenResolved(
token: token,
chainId: reboundChainId,
amountOverride: request.paymentRequest.resolveTokenAmount(token),
);
}
}
}
return TokenUnknown(contract);
}
}
abstract class AnyPayTokenLookup {
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
});
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId});
}
class TokenUtilitiesLookup implements AnyPayTokenLookup {
const TokenUtilitiesLookup();
@override
Future<CryptoCurrency?> findTokenByAddress({
required WalletType walletType,
required String address,
}) =>
TokenUtilities.findTokenByAddress(walletType: walletType, address: address);
@override
Future<int?> findEvmChainIdForContract(String contractAddress, {int? excludingChainId}) =>
TokenUtilities.findEvmChainIdForContract(contractAddress, excludingChainId: excludingChainId);
}
+141
View File
@@ -0,0 +1,141 @@
import "package:cake_wallet/core/address_validator.dart";
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayRouter {
static final List<RegExp> _swapIncompatibleAddressPatterns = [
RegExp("^(?:${AddressValidator.silentPaymentAddressPatternMainnet})\$"),
RegExp("^(?:${AddressValidator.silentPaymentAddressPatternTestnet})\$"),
RegExp("^(?:${AddressValidator.mWebAddressPattern})\$"),
];
static bool _isSwapIncompatibleAddress(String address) =>
_swapIncompatibleAddressPatterns.any((pattern) => pattern.hasMatch(address));
static AnyPayDecision route(
AnyPayRequest request,
WalletSnapshot snapshot,
AnyPayResolution resolution,
) {
if (request.rawInput.trim().isEmpty) {
return const AnyPayEmptyInput();
}
final detection = request.detection;
final detectedType = detection.detectedWalletType;
if (!detection.isValid || detectedType == null) {
return const AnyPayApplyToCurrentWallet();
}
if (detectedType == WalletType.solana && !snapshot.hasSolanaProxy) {
return const AnyPayApplyToCurrentWallet();
}
if (detectedType == WalletType.tron && !snapshot.hasTronProxy) {
return const AnyPayApplyToCurrentWallet();
}
if (isEVMCompatibleChain(detectedType)) {
if (!snapshot.hasEvmProxy) {
return const AnyPayApplyToCurrentWallet();
}
return _routeEvm(request, snapshot, resolution);
}
if (resolution is TokenUnknown) {
return AnyPayUnsupportedToken(resolution.contract);
}
if (detectedType == snapshot.type) {
return AnyPayApplyToCurrentWallet(
token: resolution is TokenResolved ? resolution.token : null,
amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
fallbackCurrency: _nativeRequestCurrency(request),
);
}
return AnyPayCrossChainPayment(
targetWalletType: detectedType,
wallets: snapshot.walletsOfType(detectedType),
token: resolution is TokenResolved ? resolution.token : null,
amountOverride: resolution is TokenResolved ? resolution.amountOverride : null,
canSwap: !_isSwapIncompatibleAddress(request.address),
);
}
static AnyPayDecision _routeEvm(
AnyPayRequest request,
WalletSnapshot snapshot,
AnyPayResolution resolution,
) {
final requestedChainId = requestedEvmChainId(request, snapshot);
if (requestedChainId == null) {
if (snapshot.currentWalletIsEvm) {
return const AnyPayApplyToCurrentWallet();
}
return const AnyPayEvmNetworkChoice();
}
if (!snapshot.supportedEvmChains.containsKey(requestedChainId)) {
return AnyPayUnsupportedNetwork(requestedChainId);
}
if (resolution is TokenUnknown) {
return AnyPayUnsupportedToken(resolution.contract);
}
int targetChainId = requestedChainId;
if (resolution is TokenResolved && resolution.chainId != null) {
targetChainId = resolution.chainId!;
}
final token = resolution is TokenResolved ? resolution.token : null;
final amountOverride = resolution is TokenResolved ? resolution.amountOverride : null;
if (snapshot.currentWalletIsEvm && snapshot.currentChainId == targetChainId) {
return AnyPayApplyToCurrentWallet(
token: token,
amountOverride: amountOverride,
fallbackCurrency: _nativeRequestCurrency(request),
);
}
final targetWalletType = snapshot.supportedEvmChains[targetChainId]!;
return AnyPayCrossChainPayment(
targetWalletType: targetWalletType,
targetChainId: targetChainId,
wallets: snapshot.walletsOfType(targetWalletType),
token: token,
amountOverride: amountOverride,
);
}
static CryptoCurrency? _nativeRequestCurrency(AnyPayRequest request) {
if (request.paymentRequest.scheme.isEmpty || request.hasContract || request.amount.isEmpty) {
return null;
}
return request.detection.detectedCurrency;
}
static int? requestedEvmChainId(AnyPayRequest request, WalletSnapshot snapshot) {
final binding = request.chainBinding;
if (binding is ExplicitEvmChain) {
return binding.chainId;
}
if (binding is ChainlessEvm) {
// the eip 681 spec says we should use the current chain id if there is no chainId in the incoming request
return snapshot.currentWalletIsEvm ? snapshot.currentChainId ?? 1 : 1;
}
return null;
}
}
+140
View File
@@ -0,0 +1,140 @@
import "package:cake_wallet/core/anypay/anypay_models.dart";
import "package:cake_wallet/core/anypay/anypay_parser.dart";
import "package:cake_wallet/core/anypay/anypay_resolver.dart";
import "package:cake_wallet/core/anypay/anypay_router.dart";
import "package:cake_wallet/evm/evm.dart";
import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
import "package:cake_wallet/reactions/wallet_connect.dart";
import "package:cake_wallet/solana/solana.dart";
import "package:cake_wallet/store/app_store.dart";
import "package:cake_wallet/tron/tron.dart";
import "package:cake_wallet/utils/payment_request.dart";
import "package:cw_core/amount/money.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/currency_for_wallet_type.dart";
import "package:cw_core/utils/print_verbose.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
class AnyPayService {
AnyPayService({
required this.appStore,
required this.walletSwitchService,
AnyPayResolver? resolver,
}) : resolver = resolver ?? AnyPayResolver();
final AppStore appStore;
final WalletSwitchService walletSwitchService;
final AnyPayResolver resolver;
Future<AnyPayEvaluation> evaluateRawInput(String input) => _evaluate(AnyPayParser.fromRaw(input));
Future<AnyPayEvaluation> evaluatePaymentRequest(PaymentRequest request) =>
_evaluate(AnyPayParser.fromPaymentRequest(request));
Future<AnyPayEvaluation> evaluateForEvmChain(AnyPayRequest request, int chainId) => _evaluate(
AnyPayRequest(
rawInput: request.rawInput,
paymentRequest: request.paymentRequest,
detection: request.detection,
chainBinding: ExplicitEvmChain(chainId),
),
);
Future<AnyPayEvaluation> _evaluate(AnyPayRequest request) async {
try {
final snapshot = await _buildWalletSnapshot();
final resolution = await resolver.resolve(request, snapshot);
return AnyPayEvaluation(
request: request,
decision: AnyPayRouter.route(request, snapshot, resolution),
);
} catch (e) {
printV("anypay evaluation failed: $e");
return AnyPayEvaluation(
request: request,
decision: const AnyPayApplyToCurrentWallet(),
);
}
}
Future<WalletSnapshot> _buildWalletSnapshot() async {
final wallet = appStore.wallet!;
final currentWalletIsEvm = isEVMCompatibleChain(wallet.type);
final supportedEvmChains = <int, WalletType>{};
if (evm != null) {
for (final chain in evm!.getAllChains()) {
final walletType = evm!.getWalletTypeByChainId(chain.chainId);
if (walletType != null) {
supportedEvmChains[chain.chainId] = walletType;
}
}
}
return WalletSnapshot(
type: wallet.type,
currentWalletIsEvm: currentWalletIsEvm,
currentChainId: currentWalletIsEvm && evm != null
? evm!.getSelectedChainId(wallet) ?? evm!.getChainIdByWalletType(wallet.type)
: null,
wallets: await WalletInfo.getAll(),
supportedEvmChains: supportedEvmChains,
hasEvmProxy: evm != null,
hasSolanaProxy: solana != null,
hasTronProxy: tron != null,
);
}
AnyPaySwapIntent buildSwapIntent(
AnyPayRequest request, {
required WalletType targetWalletType,
int? targetChainId,
CryptoCurrency? token,
}) {
final receiveCurrency = token ??
(targetChainId != null
? walletTypeToCryptoCurrency(targetWalletType, chainId: targetChainId)
: request.detection.detectedCurrency ?? walletTypeToCryptoCurrency(targetWalletType));
final amountValue = token != null
? request.paymentRequest.resolveTokenAmount(token)
: (request.amount.isNotEmpty ? request.amount : null);
return AnyPaySwapIntent(
request: request,
receiveCurrency: receiveCurrency,
targetWalletType: targetWalletType,
receiveAmount: amountValue != null ? Money.tryParse(amountValue, receiveCurrency) : null,
);
}
Future<bool> switchWalletForPayment(WalletInfo walletInfo, {int? chainId}) async {
try {
await walletSwitchService.switchToWallet(walletInfo);
} catch (e) {
printV("wallet switch failed: $e");
return false;
}
final wallet = appStore.wallet!;
if (chainId != null && evm != null && isEVMCompatibleChain(wallet.type)) {
try {
final node = appStore.settingsStore.getCurrentNode(wallet.type, chainId: chainId);
await evm!.selectChain(wallet, chainId, node: node);
} catch (e, s) {
printV("switchWalletForPayment chain select failed: $e\n$s");
return false;
}
}
try {
await wallet.updateBalance();
} catch (e) {
printV("balance refresh after wallet switch failed: $e");
}
return true;
}
}
+6
View File
@@ -238,6 +238,12 @@ class UniversalAddressDetector {
currency: CryptoCurrency.doge,
),
// Zcash (transparent, Sapling shielded, unified)
_DetectionPattern(
pattern: RegExp("^(?:${AddressValidator.zcashAddressPattern})\$"),
currency: CryptoCurrency.zec,
),
// Solana (Base58 format)
_DetectionPattern(
pattern: RegExp(r'^[1-9A-HJ-NP-Za-km-z]{43,44}$'),
+14 -6
View File
@@ -7,6 +7,7 @@ import 'package:cake_wallet/anypay/anypay_api.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart';
import 'package:cake_wallet/core/address_resolver/address_resolver_service.dart';
import 'package:cake_wallet/core/anypay/anypay_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_service.dart';
import 'package:cake_wallet/core/address_resolver/yat/yat_store.dart';
import 'package:cake_wallet/entities/bitcoin_amount_display_mode.dart';
@@ -55,12 +56,11 @@ import 'package:cake_wallet/new-ui/new_dashboard.dart';
import 'package:cake_wallet/new-ui/pages/about_page.dart';
import 'package:cake_wallet/new-ui/pages/account_customizer.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_amount_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_network_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_receiving_wallet_page.dart';
import 'package:cake_wallet/new-ui/pages/coin_control_page.dart';
import 'package:cake_wallet/new-ui/pages/addresses_page.dart';
import 'package:cake_wallet/new-ui/pages/home_page.dart';
import 'package:cake_wallet/new-ui/pages/send_page.dart';
import "package:cake_wallet/new-ui/services/wallet_switch_service.dart";
import 'package:cake_wallet/new-ui/pages/lightning_username_page.dart';
import 'package:cake_wallet/new-ui/pages/receive_page.dart';
import 'package:cake_wallet/new-ui/viewmodels/lightning_username/lightning_username_bloc.dart';
@@ -169,7 +169,6 @@ import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_detail_page.dart';
import 'package:cake_wallet/new-ui/pages/bridge/bridge_history_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
@@ -193,7 +192,6 @@ import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
import 'package:cake_wallet/store/bridge_transfers_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
import 'package:cake_wallet/store/node_list_store.dart';
import 'package:cake_wallet/store/seed_settings_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
@@ -250,7 +248,6 @@ import 'package:cake_wallet/view_model/nano_account_list/nano_account_edit_or_cr
import 'package:cake_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart';
import 'package:cake_wallet/view_model/node_list/pow_node_list_view_model.dart';
import 'package:cake_wallet/view_model/order_details_view_model.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
@@ -926,7 +923,8 @@ Future<void> setup({
authService: getIt.get<AuthService>(),
params: params,
contactListViewModel: getIt.get<ContactListViewModel>(),
paymentViewModel: getIt.get<PaymentViewModel>(),
anyPayService: getIt.get<AnyPayService>(),
linkViewModel: getIt.get<LinkViewModel>(),
walletSwitcherViewModel: getIt.get<WalletSwitcherViewModel>(),
);
});
@@ -1291,6 +1289,16 @@ Future<void> setup({
appStore: getIt.get<AppStore>(),
));
getIt.registerFactory(() => WalletSwitchService(
walletLoadingService: getIt.get<WalletLoadingService>(),
appStore: getIt.get<AppStore>(),
));
getIt.registerFactory(() => AnyPayService(
appStore: getIt.get<AppStore>(),
walletSwitchService: getIt.get<WalletSwitchService>(),
));
getIt.registerFactory(() => WalletSwitcherViewModel(
appStore: getIt.get<AppStore>(),
walletLoadingService: getIt.get<WalletLoadingService>(),
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,
),
],
),
),
],
),
),
);
}
}
@@ -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,
),
);
@@ -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,
),
),
],
),
),
],
);
}
}
+4 -5
View File
@@ -149,7 +149,6 @@ import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_mode
import 'package:cake_wallet/view_model/hardware_wallet/trezor_connect_view_model.dart';
import 'package:cake_wallet/view_model/monero_account_list/account_list_item.dart';
import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/wallet_groups_display_view_model.dart';
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
import 'package:cake_wallet/view_model/wallet_hardware_restore_view_model.dart';
@@ -445,6 +444,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.send:
final args = settings.arguments as Map<String, dynamic>?;
final initialPaymentRequest = args?['paymentRequest'] as PaymentRequest?;
final initialRawInput = args?["rawLink"] as String?;
final coinTypeToSpendFrom = args?['coinTypeToSpendFrom'] as UnspentCoinType?;
return handleRouteWithPlatformAwareness(
@@ -452,6 +452,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
child: getIt.get<NewSendPage>(
param1: SendPageParams(
initialPaymentRequest: initialPaymentRequest,
initialRawInput: initialRawInput,
unspentCoinType: coinTypeToSpendFrom ?? UnspentCoinType.any),
),
),
@@ -473,7 +474,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.newReceivePage:
if (FeatureFlag.hasNewUi) {
return handleRouteWithPlatformAwareness(
(context) => Material(child: getIt.get<NewReceivePage>(param1: false, param2: null)),
(context) => Material(child: getIt.get<NewReceivePage>(param1: false, param2: null)),
settings: settings,
);
}
@@ -835,9 +836,7 @@ Route<dynamic> createRoute(RouteSettings settings) {
final useTestnet = args['useTestnet'] as bool;
final toggleTestnet = args['toggleTestnet'] as Function(bool? val);
final zcashNetwork = args['zcashNetwork'] as int? ?? ZcashNetworkType.mainnet;
final setZcashNetwork =
args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
final restoredWallet = args['restoredWallet'] as RestoredWallet?;
final setZcashNetwork = args['setZcashNetwork'] as void Function(int network)? ?? (_) {};
final viewModelParam = {'type': type, 'isPow': false};
+25
View File
@@ -125,6 +125,31 @@ class PaymentRequest {
);
}
PaymentRequest copyWith({
String? address,
String? amount,
String? note,
String? scheme,
String? pjUri,
String? callbackUrl,
String? callbackMessage,
String? contractAddress,
int? chainId,
String? rawTokenAmount,
}) =>
PaymentRequest(
address ?? this.address,
amount ?? this.amount,
note ?? this.note,
scheme ?? this.scheme,
pjUri ?? this.pjUri,
callbackUrl: callbackUrl ?? this.callbackUrl,
callbackMessage: callbackMessage ?? this.callbackMessage,
contractAddress: contractAddress ?? this.contractAddress,
chainId: chainId ?? this.chainId,
rawTokenAmount: rawTokenAmount ?? this.rawTokenAmount,
);
final String address;
final String amount;
final String note;
@@ -2,12 +2,10 @@ import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:developer';
import 'dart:math' show min;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/address_validator.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/create_trade_result.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
@@ -415,6 +413,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@observable
bool tradeStarted = false;
@observable
bool noProviderForPair = false;
@observable
bool isSendFromExternal = false;
@@ -980,6 +981,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
bestRate = _sortedAvailableProviders.keys.first;
bestRateProvider = _sortedAvailableProviders.values.first;
}
noProviderForPair = _sortedAvailableProviders.isEmpty;
}
@action
@@ -1603,24 +1605,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
String? _addressTypeValidation(String refundAddress, String receiveAddress) {
final isRefundAddressSP =
RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(refundAddress);
if (isRefundAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_refund}';
final isReceiveAddressSP =
RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(receiveAddress);
if (isReceiveAddressSP) return 'Silent Payment ${S.current.address_not_allowed_as_receive}';
final isRefundAddressMWEB = RegExp(AddressValidator.mWebAddressPattern).hasMatch(refundAddress);
if (isRefundAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_refund}';
final isReceiveAddressMWEB =
RegExp(AddressValidator.mWebAddressPattern).hasMatch(receiveAddress);
if (isReceiveAddressMWEB) return 'MWEB ${S.current.address_not_allowed_as_receive}';
return null;
}
void _defineIsReceiveAmountEditable() {
/*if ((provider is ChangeNowExchangeProvider)
&&(depositCurrency == CryptoCurrency.xmr)
+21 -2
View File
@@ -25,6 +25,10 @@ class LinkViewModel {
Uri? currentLink;
bool readyToOpenPage = false;
final StreamController<Uri> _incomingPaymentLinksController = StreamController<Uri>.broadcast();
Stream<Uri> get incomingPaymentLinks => _incomingPaymentLinksController.stream;
bool get _isValidPaymentUri => currentLink?.path.isNotEmpty ?? false;
bool get isWalletConnectLink => currentLink?.authority == 'wc';
bool get isNanoGptLink => currentLink?.scheme == 'nano-gpt';
@@ -96,7 +100,10 @@ class LinkViewModel {
}
if (_isValidPaymentUri) {
return {"paymentRequest": PaymentRequest.fromUri(currentLink)};
return {
"paymentRequest": PaymentRequest.fromUri(currentLink),
"rawLink": currentLink.toString(),
};
}
return null;
@@ -132,8 +139,20 @@ class LinkViewModel {
return;
}
// Prevent navigating to the same route again.
if (route == Routes.send &&
!isQuickActionLink &&
currentLink != null &&
_incomingPaymentLinksController.hasListener) {
final link = currentLink!;
currentLink = null;
_incomingPaymentLinksController.add(link);
return;
}
// Prevent navigating to the same route again and clear the link so a later auth
// event cannot refire it.
if (appStore.currentRouteName == route) {
currentLink = null;
return;
}
+5
View File
@@ -1730,6 +1730,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
@action
void applyAnyPayCurrency(CryptoCurrency currency) {
selectedCryptoCurrency = currency;
}
String _decodeMethodSelector(String s) =>
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
}