Refactor WalletConnect UI components and enhance transaction approval and signing flow (#3233)

* Refactor WalletConnect UI components and enhance transaction approval and signing flow

* Refactor WalletConnect UI components and enhance transaction approval and signing flow

* Remove unused ui components

* Enhance pairing details page and remove unused ui components

* adjust buttons on details page

* Add warning banner for scam dApps on connection requests

* General cleanup

* Parse and display estimated network fee for WalletConnect approval requests

* Revamp WalletConnect pair listing view

* Remove unused action buttons and walletkit methods

* update svg and sync with dev
This commit is contained in:
David Adegoke
2026-06-01 17:27:24 +02:00
committed by GitHub
parent 5cccde53f6
commit e13d997622
67 changed files with 2127 additions and 1329 deletions
@@ -29,7 +29,6 @@ class BottomSheetServiceImpl implements BottomSheetService {
int closeAfter = 0,
bool isModalDismissible = false,
}) async {
// Create the bottom sheet queue item
final completer = Completer<dynamic>();
final queueItem = BottomSheetQueueItemModel(
widget: widget,
@@ -38,15 +37,12 @@ class BottomSheetServiceImpl implements BottomSheetService {
isModalDismissible: isModalDismissible,
);
// If the current sheet it null, set it to the queue item
if (currentSheet.value == null) {
currentSheet.value = queueItem;
} else {
// Otherwise, add it to the queue
queue.add(queueItem);
}
// Return the future
return await completer.future;
}
@@ -1,6 +1,10 @@
import 'dart:convert';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:eth_sig_util/eth_sig_util.dart';
import 'package:eth_sig_util/util/utils.dart';
@@ -106,7 +110,6 @@ class EvmChainServiceImpl {
if (isApproved) {
try {
// Load the private key
final keys = wcKeyService.getKeysForChain(appStore.wallet!);
final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
@@ -155,7 +158,6 @@ class EvmChainServiceImpl {
if (isApproved) {
try {
// Load the private key
final keys = wcKeyService.getKeysForChain(appStore.wallet!);
final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
@@ -297,7 +299,6 @@ class EvmChainServiceImpl {
if (transaction is Transaction) {
try {
// Load the private key
final keys = wcKeyService.getKeysForChain(appStore.wallet!);
final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
@@ -309,7 +310,6 @@ class EvmChainServiceImpl {
chainId: int.parse(chainId),
);
// Sign the transaction
final signedTx = bytesToHex(signature, include0x: true);
response = response.copyWith(result: signedTx);
} on RPCError catch (e) {
@@ -349,7 +349,6 @@ class EvmChainServiceImpl {
);
if (transaction is Transaction) {
try {
// Load the private key
final keys = wcKeyService.getKeysForChain(appStore.wallet!);
final credentials = EthPrivateKey.fromHex(keys[0].privateKey);
final chainId = getChainId().split(':').last;
@@ -440,30 +439,26 @@ class EvmChainServiceImpl {
transaction = await _applyWCBufferedFees(transaction);
final gweiGasPrice =
(transaction.gasPrice?.getInWei ?? transaction.maxFeePerGas?.getInWei ?? BigInt.zero) /
BigInt.from(1000000000);
final nativeCurrency = evm?.getChainInfoByChainId(reference.chainId ?? 1)?.currency;
final nativeSymbol = nativeCurrency?.title ?? 'ETH';
final amount = (transaction.value?.getInWei ?? BigInt.zero) / BigInt.from(1e18);
final txMessageText = '${S.current.value}: ${amount.toStringAsFixed(9)} ETH\n'
final txMessageText = '${S.current.value}: ${amount.toStringAsFixed(9)} $nativeSymbol\n'
'${S.current.from}: ${transaction.from?.hex}\n'
'${S.current.to}: ${transaction.to?.hex}';
final feeRows = _buildFeeExtraModels(transaction, nativeCurrency, nativeSymbol);
if (await MethodsUtils.requestApproval(
txMessageText,
title: title,
method: method,
chainId: chainId,
address: address,
address: address ?? transaction.from?.hex ?? '',
transportType: transportType,
verifyContext: verifyContext,
extraModels: [
WCConnectionModel(
title: S.current.gas_price,
elements: ['${gweiGasPrice.toStringAsFixed(2)} GWEI'],
),
],
extraModels: feeRows,
)) {
return transaction;
}
@@ -471,6 +466,66 @@ class EvmChainServiceImpl {
return JsonRpcError(code: 5002, message: S.current.user_rejected_method);
}
List<WCConnectionModel> _buildFeeExtraModels(
Transaction transaction,
CryptoCurrency? nativeCurrency,
String nativeSymbol,
) {
final gasLimit = transaction.maxGas;
if (gasLimit == null || gasLimit <= 0) return const [];
final gasLimitBig = BigInt.from(gasLimit);
final isEip1559 = transaction.isEIP1559;
final perGasWei =
isEip1559 ? transaction.maxFeePerGas?.getInWei : transaction.gasPrice?.getInWei;
if (perGasWei == null || perGasWei <= BigInt.zero) return const [];
final feeWei = gasLimitBig * perGasWei;
final feeNative = feeWei.toDouble() / 1e18;
final label = isEip1559 ? S.current.wc_max_network_fee : S.current.wc_network_fee;
return [
WCConnectionModel(
title: label,
elements: [_formatFeeLine(feeNative, nativeSymbol, nativeCurrency)],
),
];
}
String _formatFeeLine(
double feeNative,
String nativeSymbol,
CryptoCurrency? nativeCurrency,
) {
final cryptoPart = '${_formatNativeAmount(feeNative)} $nativeSymbol';
if (nativeCurrency == null) return cryptoPart;
try {
final fiatStore = getIt.get<FiatConversionStore>();
final price = fiatStore.prices[nativeCurrency];
if (price == null || price <= 0) return cryptoPart;
final fiatSymbol = appStore.settingsStore.fiatCurrency.title;
final fiatValue = calculateFiatAmount(
price: price,
cryptoAmount: feeNative.toString(),
);
if (fiatValue.isEmpty || fiatValue == '0.00') return cryptoPart;
return '$cryptoPart (~ $fiatValue $fiatSymbol)';
} catch (_) {
return cryptoPart;
}
}
String _formatNativeAmount(double value) {
if (value == 0) return '0';
if (value >= 0.0001) return value.toStringAsFixed(6);
return value.toStringAsExponential(4);
}
Future<Transaction> _ensureWCTransactionHasGasLimit(Transaction transaction) async {
final hasGasLimit = transaction.maxGas != null && transaction.maxGas! > 0;
if (hasGasLimit) return transaction;
@@ -637,7 +692,6 @@ $messageDetails''';
if (value == null) continue;
if (types.containsKey(fieldType)) {
// Handle nested types
final nestedFields = types[fieldType] as List<dynamic>;
if (fieldType == 'Person') {
// Special formatting for Person type
@@ -80,7 +80,6 @@ class SolanaChainService {
error: JsonRpcError(code: error.code, message: error.message),
);
}
//
} catch (e) {
debugPrint('solanaSignMessage error $e');
final error = Errors.getSdkError(Errors.MALFORMED_REQUEST_PARAMS);
@@ -6,12 +6,7 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
abstract class WalletConnectKeyService {
/// Returns a list of all the keys.
List<ChainKeyModel> getKeys(WalletBase wallet);
/// Returns a list of all the keys for a given chain id.
/// If the chain is not found, returns an empty list.
/// - [chain]: The chain to get the keys for.
List<ChainKeyModel> getKeysForChain(WalletBase wallet);
}
@@ -20,10 +20,9 @@ import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/chai
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/eth_utils.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/method_utils.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_request_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_message_display_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_request_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_session_auth_request_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_request_sheet.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_signing_request_sheet.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'bottom_sheet_service.dart';
@@ -56,8 +55,6 @@ abstract class WalletKitServiceBase with Store {
bool isInitialized;
/// The list of requests from the dapp
/// Potential types include, but aren't limited to:
/// [SessionProposalEvent], [SessionAuthRequest]
@observable
ObservableList<PairingInfo> pairings;
@@ -69,7 +66,6 @@ abstract class WalletKitServiceBase with Store {
@action
void create() {
// Create the walletkit client
_walletKit = ReownWalletKit(
core: ReownCore(
projectId: secrets.walletConnectProjectId,
@@ -85,7 +81,6 @@ abstract class WalletKitServiceBase with Store {
_walletKit.core.addLogListener(_logListener);
// Setup our listeners
log('Created instance of walletKit');
_walletKit.core.pairing.onPairingInvalid.subscribe(_onPairingInvalid);
@@ -288,16 +283,12 @@ abstract class WalletKitServiceBase with Store {
if (args != null) {
final proposer = args.params.proposer;
final result = (await _bottomSheetHandler.queueBottomSheet(
widget: WCRequestWidget(
widget: WCConnectionRequestSheet(
proposalData: args.params,
requester: proposer,
verifyContext: args.verifyContext,
child: WCConnectionRequestWidget(
proposalData: args.params,
verifyContext: args.verifyContext,
requester: proposer,
walletKeyService: walletKeyService,
walletKit: walletKit,
appStore: appStore,
),
walletKeyService: walletKeyService,
appStore: appStore,
),
)) ??
WCBottomSheetResult.reject;
@@ -400,10 +391,8 @@ abstract class WalletKitServiceBase with Store {
debugPrint('_onPairingCreate $args');
if (args != null && args.topic != null && args.topic!.isNotEmpty) {
// Save the pairing topic when pairing is created
savePairingTopicToLocalStorage(args.topic!);
// Refresh pairings to show the new pairing in the list
_refreshPairings();
}
}
@@ -440,16 +429,28 @@ abstract class WalletKitServiceBase with Store {
formattedMessages.add({iss: message});
}
final requesterMetadata = args.requester.metadata;
final requesterIcon = requesterMetadata.icons.isNotEmpty
? requesterMetadata.icons.first
: null;
final chainKeysForAuth = walletKeyService.getKeysForChain(appStore.wallet!);
final addressForAuth =
chainKeysForAuth.isNotEmpty ? chainKeysForAuth.first.publicKey : '';
final combinedMessageBody =
formattedMessages.map((m) => m.values.first as String).join('\n\n');
final WCBottomSheetResult result = (await _bottomSheetHandler.queueBottomSheet(
widget: WCSessionAuthRequestWidget(
child: WCConnectionRequestWidget(
sessionAuthPayload: newAuthPayload,
verifyContext: args.verifyContext,
requester: args.requester,
walletKeyService: walletKeyService,
walletKit: _walletKit,
appStore: appStore,
),
widget: WCSigningRequestSheet(
title: S.current.wc_signing_request_title,
swipeLabel: S.current.wc_swipe_to_sign,
dappName: requesterMetadata.name,
dappIconUrl: requesterIcon,
dappSubtitle: requesterMetadata.url,
message: combinedMessageBody,
walletName: appStore.wallet?.name ?? '',
address: addressForAuth,
verifyContext: args.verifyContext,
signAllCount: formattedMessages.length,
),
) as WCBottomSheetResult?) ??
WCBottomSheetResult.reject;
@@ -458,10 +459,8 @@ abstract class WalletKitServiceBase with Store {
final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
final privateKey = '0x${chainKeys.first.privateKey}';
final credentials = EthPrivateKey.fromHex(privateKey);
//
final messageToSign = formattedMessages.length;
final count = (result == WCBottomSheetResult.one) ? 1 : messageToSign;
//
final List<Cacao> cacaos = [];
for (var i = 0; i < count; i++) {
final iss = formattedMessages[i].keys.first;
@@ -572,19 +571,6 @@ abstract class WalletKitServiceBase with Store {
_refreshPairings();
}
@action
Future<void> updateSession({
required String topic,
required Map<String, Namespace> namespaces,
}) async {
await walletKit.updateSession(topic: topic, namespaces: namespaces);
}
@action
Future<void> extendSession({required String topic}) async {
await walletKit.extendSession(topic: topic);
}
@action
Future<void> pairWithUri(Uri uri) async {
try {
@@ -633,7 +619,7 @@ abstract class WalletKitServiceBase with Store {
@action
List<SessionData> getSessionsForPairingInfo(PairingInfo pairing) {
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
return sessions.where((element) => element.pairingTopic == pairing.topic).toList();
}
String getKeyForStoringTopicsForWallet() {
@@ -664,18 +650,14 @@ abstract class WalletKitServiceBase with Store {
}
List<String> getPairingTopicsForWallet(String key) {
// Get the JSON-encoded string from shared preferences
final jsonString = sharedPreferences.getString(key);
// If the string is null, return an empty list
if (jsonString == null) {
return [];
}
// Decode the JSON string to a list of strings
final List<dynamic> jsonList = jsonDecode(jsonString) as List<dynamic>;
// Cast each item to a string
return jsonList.map((item) => item as String).toList();
}
@@ -691,12 +673,10 @@ abstract class WalletKitServiceBase with Store {
}
Future<void> savePairingTopicToLocalStorage(String pairingTopic) async {
// Get key specific to the current wallet
final key = getKeyForStoringTopicsForWallet();
if (key.isEmpty) return;
// Get all pairing topics attached to this key
final pairingTopicsForWallet = getPairingTopicsForWallet(key);
bool isPairingTopicAlreadySaved = pairingTopicsForWallet.contains(pairingTopic);
@@ -704,13 +684,10 @@ abstract class WalletKitServiceBase with Store {
'Is Pairing Topic Saved: $isPairingTopicAlreadySaved, Key: $key, Topic: $pairingTopic');
if (!isPairingTopicAlreadySaved) {
// Update the list with the most recent pairing topic
pairingTopicsForWallet.add(pairingTopic);
// Convert the list of updated pairing topics to a JSON-encoded string
final jsonString = jsonEncode(pairingTopicsForWallet);
// Save the encoded string to shared preferences
await sharedPreferences.setString(key, jsonString);
}
}
@@ -3,8 +3,9 @@ import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_request_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_message_card.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_signing_request_sheet.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/themes/core/custom_theme_colors.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
@@ -12,6 +13,14 @@ import 'package:reown_walletkit/reown_walletkit.dart';
class MethodsUtils {
static final walletKit = getIt.get<WalletKitService>().walletKit;
static final bottomSheetService = getIt.get<BottomSheetService>();
static const _transactionMethods = {
'eth_sendTransaction',
'eth_signTransaction',
'solana_signTransaction',
'solana_signAllTransactions',
'solana_signAndSendTransaction',
};
static Future<bool> requestApproval(
String text, {
@@ -23,23 +32,53 @@ class MethodsUtils {
List<WCConnectionModel> extraModels = const [],
VerifyContext? verifyContext,
}) async {
final appStore = getIt.get<AppStore>();
final pending = walletKit.pendingRequests.getAll();
final session = pending.isNotEmpty
? walletKit.sessions.get(pending.last.topic)
: null;
final dAppMetadata = session?.peer.metadata;
final isTransaction = method != null && _transactionMethods.contains(method);
final resolvedTitle = title ??
(isTransaction
? S.current.wc_approve_request_title
: S.current.wc_signing_request_title);
final swipeLabel =
isTransaction ? S.current.wc_swipe_to_approve : S.current.wc_swipe_to_sign;
final extraRows = <WCMessageRow>[];
if (method != null && method.isNotEmpty) {
extraRows.add(WCMessageRow(label: S.current.method, value: method));
}
if (chainId != null && chainId.isNotEmpty) {
extraRows.add(WCMessageRow(label: S.current.chain_id, value: chainId));
}
if (transportType.isNotEmpty) {
extraRows.add(WCMessageRow(
label: S.current.transport_type,
value: transportType.toUpperCase(),
));
}
for (final model in extraModels) {
if (model.title == null) continue;
final value = model.elements?.join(', ') ?? model.text ?? '';
extraRows.add(WCMessageRow(label: model.title!, value: value));
}
final WCBottomSheetResult result = (await bottomSheetService.queueBottomSheet(
widget: WCRequestWidget(
widget: WCSigningRequestSheet(
title: resolvedTitle,
swipeLabel: swipeLabel,
dappName: dAppMetadata?.name ?? '',
dappIconUrl:
(dAppMetadata?.icons.isNotEmpty ?? false) ? dAppMetadata!.icons.first : null,
dappSubtitle: method ?? dAppMetadata?.url ?? '',
message: text,
walletName: appStore.wallet?.name ?? '',
address: address ?? '',
verifyContext: verifyContext,
child: WCConnectionWidget(
title: title ?? S.current.approve_request,
info: [
WCConnectionModel(
title: '${S.current.method}: $method\n'
'${S.current.transport_type}: ${transportType.toUpperCase()}\n'
'${S.current.chain_id}: $chainId\n'
'${S.current.address}: $address\n\n'
'${S.current.message}:',
elements: [text],
),
...extraModels,
],
),
extraRows: extraRows,
),
) as WCBottomSheetResult?) ??
WCBottomSheetResult.reject;
@@ -1,76 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class ConnectionWidgetBuilder {
static List<WCConnectionWidget> buildFromRequiredNamespaces(
Map<String, Namespace> generatedNamespaces,
) {
final List<WCConnectionWidget> views = [];
for (final key in generatedNamespaces.keys) {
final namespaces = generatedNamespaces[key]!;
final chains = NamespaceUtils.getChainsFromAccounts(namespaces.accounts);
final List<WCConnectionModel> models = [];
// If the chains property is present, add the chain data to the models
models.add(WCConnectionModel(title: S.current.chains, elements: chains));
models.add(WCConnectionModel(title: S.current.methods, elements: namespaces.methods));
if (namespaces.events.isNotEmpty) {
models.add(WCConnectionModel(title: S.current.events, elements: namespaces.events));
}
views.add(WCConnectionWidget(title: key, info: models));
}
return views;
}
static List<WCConnectionWidget> buildFromNamespaces(
String topic,
Map<String, Namespace> namespaces,
BuildContext context,
) {
final List<WCConnectionWidget> views = [];
for (final key in namespaces.keys) {
final ns = namespaces[key]!;
final List<WCConnectionModel> models = [];
// If the chains property is present, add the chain data to the models
models.add(WCConnectionModel(title: S.current.accounts, elements: ns.accounts));
models.add(WCConnectionModel(title: S.current.methods, elements: ns.methods));
if (ns.events.isNotEmpty) {
models.add(WCConnectionModel(title: S.current.events, elements: ns.events));
}
views.add(WCConnectionWidget(title: key, info: models));
}
return views;
}
static Map<String, Namespace> updateNamespaces(
Map<String, Namespace> currentNamespaces,
String namespace,
List<String> newChains,
) {
final updatedNamespaces = Map<String, Namespace>.from(currentNamespaces);
final accounts = currentNamespaces[namespace]!.accounts;
final address = NamespaceUtils.getAccount(accounts.first);
final newAccounts = newChains.map((c) => '$c:$address').toList();
final newNamespaces = currentNamespaces[namespace]!.copyWith(
chains: NamespaceUtils.getChainsFromAccounts(accounts)..addAll(newChains),
accounts: List<String>.from(accounts)..addAll(newAccounts),
);
updatedNamespaces[namespace] = newNamespaces;
return updatedNamespaces;
}
}
@@ -0,0 +1,80 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCPermission {
const WCPermission({required this.iconUrl, required this.label});
final String iconUrl;
final String label;
}
class WCPermissionsMapper {
static const _transactionMethods = {
'eth_sendTransaction',
'eth_signTransaction',
'solana_signTransaction',
'solana_signAllTransactions',
'solana_signAndSendTransaction',
};
static const _messageSigningMethods = {
'personal_sign',
'eth_sign',
'eth_signTypedData',
'eth_signTypedData_v1',
'eth_signTypedData_v3',
'eth_signTypedData_v4',
'solana_signMessage',
};
static const _chainAdminMethods = {
'wallet_switchEthereumChain',
'wallet_addEthereumChain',
};
static List<WCPermission> fromGeneratedNamespaces(Map<String, Namespace> generatedNamespaces) {
final methods = <String>{};
for (final ns in generatedNamespaces.values) {
methods.addAll(ns.methods);
}
final permissions = <WCPermission>[
WCPermission(iconUrl: "assets/new-ui/global_view.svg", label: S.current.wc_permission_view_balance),
];
final wantsTransactionApproval = methods.any(_transactionMethods.contains);
if (wantsTransactionApproval) {
permissions.add(WCPermission(
iconUrl: "assets/new-ui/green_check.svg",
label: S.current.wc_permission_request_approval,
));
}
final wantsMessageSigning = methods.any(_messageSigningMethods.contains);
if (wantsMessageSigning) {
permissions.add(WCPermission(
iconUrl: "assets/new-ui/pencil.svg",
label: S.current.wc_permission_sign_messages,
));
}
final wantsChainAdmin = methods.any(_chainAdminMethods.contains);
if (wantsChainAdmin) {
permissions.add(WCPermission(
iconUrl: "assets/new-ui/exchange_providers.svg",
label: S.current.wc_permission_switch_chains,
));
}
final known = {..._transactionMethods, ..._messageSigningMethods, ..._chainAdminMethods};
final unknown = methods.where((m) => !known.contains(m)).toList()..sort();
for (final method in unknown) {
permissions.add(WCPermission(
iconUrl: "assets/new-ui/help.svg",
label: S.current.wc_permission_other(method),
));
}
return permissions;
}
}
@@ -1,122 +1,86 @@
import 'package:cake_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/new-ui/widgets/modern_button.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/enter_wallet_connect_uri_widget.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_hero_card.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/src/widgets/new_list_row/new_list_section.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import 'widgets/wc_pairing_item_widget.dart';
import 'wc_pairing_detail_page.dart';
class WalletConnectConnectionsView extends StatelessWidget {
final WalletKitService walletKitService;
WalletConnectConnectionsView({required this.walletKitService, Uri? launchUri, Key? key})
: super(key: key) {
WalletConnectConnectionsView({
required this.walletKitService,
Uri? launchUri,
Key? key,
}) : super(key: key) {
_triggerPairingFromDeeplink(launchUri);
}
final WalletKitService walletKitService;
void _triggerPairingFromDeeplink(Uri? launchUri) async {
if (launchUri == null) return;
if(launchUri.scheme == "wc") {
if (launchUri.scheme == 'wc') {
await walletKitService.pairWithUri(launchUri);
return;
}
final actualLinkList = launchUri.query.split("uri=");
final actualLinkList = launchUri.query.split('uri=');
if (actualLinkList.length <= 1) return;
final query = actualLinkList[1];
final decoded = Uri.decodeComponent(query).trim();
final decoded = Uri.decodeComponent(actualLinkList[1]).trim();
final sanitized = decoded.startsWith('@') ? decoded.substring(1) : decoded;
final uriData = Uri.tryParse(sanitized);
if (uriData == null || (uriData.scheme.isEmpty)) return;
if (uriData == null || uriData.scheme.isEmpty) return;
await walletKitService.pairWithUri(uriData);
}
@override
Widget build(BuildContext context) {
return WCPairingsWidget(walletKitService: walletKitService);
}
}
class WCPairingsWidget extends BasePage {
WCPairingsWidget({required this.walletKitService, Key? key})
: walletKit = walletKitService.walletKit;
final ReownWalletKit walletKit;
final WalletKitService walletKitService;
@override
String get title => S.current.walletConnect;
Future<void> _onScanQrCode(BuildContext context, ReownWalletKit web3Wallet) async {
final String? uri;
if (DeviceInfo.instance.isMobile) {
bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
uri = await presentQRScanner(context);
} else {
uri = await _showEnterWalletConnectURIPopUp(context);
}
Future<void> _onScanQrCode(BuildContext context) async {
final isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
final uri = await presentQRScanner(context);
await _handleWalletConnectURI(uri, context);
}
Future<String?> _showEnterWalletConnectURIPopUp(BuildContext context) async {
final walletConnectURI = await showPopUp<String>(
Future<void> _onPasteLink(BuildContext context) async {
final uri = await showPopUp<String>(
context: context,
builder: (BuildContext context) {
return EnterWalletConnectURIWrapperWidget();
},
builder: (BuildContext context) => EnterWalletConnectURIWrapperWidget(),
);
return walletConnectURI;
await _handleWalletConnectURI(uri, context);
}
Future<void> _handleWalletConnectURI(
String? walletConnectURI,
BuildContext context,
) async {
Future<void> _handleWalletConnectURI(String? walletConnectURI, BuildContext context) async {
if (walletConnectURI == null) return _invalidUriToast(context, S.current.nullURIError);
log('_onFoundUri: $walletConnectURI');
// Accept either a raw WC URI or a full URL containing `uri=` parameter
String input = walletConnectURI.trim();
if (input.contains('uri=')) {
final parts = input.split('uri=');
if (parts.length > 1) {
input = Uri.decodeComponent(parts.last);
}
if (parts.length > 1) input = Uri.decodeComponent(parts.last);
}
if (input.startsWith('@')) input = input.substring(1);
// Some scanners may prefix with '@', strip it
if (input.startsWith('@')) {
input = input.substring(1);
}
final Uri? uriData = Uri.tryParse(input);
final bool hasValidScheme = uriData != null && uriData.scheme.isNotEmpty;
if (!hasValidScheme) {
final uriData = Uri.tryParse(input);
if (uriData == null || uriData.scheme.isEmpty) {
return _invalidUriToast(context, S.current.invalid_input);
}
await walletKitService.pairWithUri(uriData);
}
@@ -135,88 +99,132 @@ class WCPairingsWidget extends BasePage {
);
}
@override
Widget body(BuildContext context) {
return Observer(
builder: (context) {
return Column(
children: [
Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
SizedBox(height: 24),
Text(
S.current.connectWalletPrompt,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface,
),
),
SizedBox(height: 16),
PrimaryButton(
text: S.current.newConnection,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
onPressed: () => _onScanQrCode(context, walletKit),
),
SizedBox(height: 4),
TextButton(
onPressed: () async {
final uri = await _showEnterWalletConnectURIPopUp(context);
await _handleWalletConnectURI(uri, context);
},
child: Text(
'Click to paste WalletConnect Link',
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
),
SizedBox(height: 16),
Expanded(
child: Visibility(
visible: walletKitService.pairings.isEmpty,
child: Center(
child: Text(
S.current.activeConnectionsPrompt,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.normal,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
replacement: ListView.builder(
itemCount: walletKitService.pairings.length,
itemBuilder: (BuildContext context, int index) {
final pairing = walletKitService.pairings[index];
return WCPairingItemWidget(
key: ValueKey(pairing.topic),
pairing: pairing,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WalletConnectPairingDetailsPage(
pairing: pairing,
walletKitService: walletKitService,
),
),
);
},
);
},
),
),
),
SizedBox(height: 48),
],
);
},
void _openPairingDetails(BuildContext context, PairingInfo pairing) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => WalletConnectPairingDetailsPage(
pairing: pairing,
walletKitService: walletKitService,
),
),
);
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
final isMobile = DeviceInfo.instance.isMobile;
return Scaffold(
backgroundColor: colors.surface,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: ModernButton(
size: 40,
onPressed: () => Navigator.of(context).maybePop(),
icon: Icon(Icons.arrow_back_ios_new, size: 16),
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const WCHeroCard(),
const SizedBox(height: 24),
Observer(
builder: (_) {
if (walletKitService.pairings.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Center(
child: Text(
S.of(context).activeConnectionsPrompt,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
),
);
}
final items = <ListItemRegularRow>[];
for (final pairing in walletKitService.pairings) {
final metadata = pairing.peerMetadata;
if (metadata == null) continue;
items.add(
ListItemRegularRow(
keyValue: pairing.topic,
label: metadata.name,
subtitle: metadata.url,
iconPath: metadata.icons.isNotEmpty
? metadata.icons.first
: 'assets/new-ui/walletconnect_icon.svg',
onTap: () => _openPairingDetails(context, pairing),
leadingIconSize: 36,
leadingIconErrorWidget: CakeImageWidget(
imageUrl: 'assets/new-ui/walletconnect_icon.svg',
width: 36,
height: 36,
),
),
);
}
if (items.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Center(
child: Text(
S.of(context).activeConnectionsPrompt,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: colors.onSurfaceVariant,
),
),
),
);
}
return NewListSections(sections: {'': items});
},
),
],
),
),
),
const SizedBox(height: 16),
NewPrimaryButton(
onPressed: () => _onPasteLink(context),
text: S.of(context).wc_paste_link,
color: colors.surfaceContainerHigh,
textColor: colors.primary,
),
if (isMobile) ...[
const SizedBox(height: 12),
NewPrimaryButton(
onPressed: () => _onScanQrCode(context),
text: S.of(context).wc_scan_qr,
color: colors.primary,
textColor: colors.onPrimary,
),
],
const SizedBox(height: 16),
],
),
),
),
);
}
}
@@ -1,15 +1,18 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/wc_permissions_mapper.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_dapp_card.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_permissions_card.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_wallet_card.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
import 'utils/namespace_model_builder.dart';
class WalletConnectPairingDetailsPage extends StatefulWidget {
final PairingInfo pairing;
final WalletKitService walletKitService;
@@ -25,180 +28,100 @@ class WalletConnectPairingDetailsPage extends StatefulWidget {
}
class WalletConnectPairingDetailsPageState extends State<WalletConnectPairingDetailsPage> {
List<Widget> sessionWidgets = [];
late String expiryDate;
List<SessionData> sessions = const [];
@override
void initState() {
super.initState();
initDateTime();
WidgetsBinding.instance.addPostFrameCallback((_) {
initSessions();
if (!mounted) return;
setState(() {
sessions = widget.walletKitService.getSessionsForPairingInfo(widget.pairing);
});
});
}
void initDateTime() {
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(widget.pairing.expiry * 1000);
int year = dateTime.year;
int month = dateTime.month;
int day = dateTime.day;
expiryDate = '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
}
void initSessions() {
List<SessionData> sessions = widget.walletKitService.getSessionsForPairingInfo(widget.pairing);
for (final SessionData session in sessions) {
List<Widget> namespaceWidget = ConnectionWidgetBuilder.buildFromNamespaces(
session.topic,
session.namespaces,
context,
);
// Loop through and add the namespace widgets, but put 20 pixels between each one
for (int i = 0; i < namespaceWidget.length; i++) {
sessionWidgets.add(namespaceWidget[i]);
if (i != namespaceWidget.length - 1) {
sessionWidgets.add(const SizedBox(height: 20.0));
}
}
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
sessionWidgets.add(
PrimaryButton(
onPressed: () async {
try {
await widget.walletKitService.extendSession(
topic: session.topic,
);
} catch (e) {
debugPrint('${e.toString()}');
}
},
text: S.current.extend_session,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
);
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
sessionWidgets.add(
PrimaryButton(
onPressed: () async {
try {
await widget.walletKitService.updateSession(
topic: session.topic,
namespaces: session.namespaces,
);
} catch (e) {
debugPrint('${e.toString()}');
}
},
text: S.current.update_session,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
);
sessionWidgets.add(const SizedBox.square(dimension: 10.0));
sessionWidgets.add(
PrimaryButton(
onPressed: () async {
try {
await widget.walletKitService.disconnectSession(
topic: session.topic,
);
} catch (e) {
debugPrint('${e.toString()}');
}
},
text: S.current.disconnect_session,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
);
}
final dateTime = DateTime.fromMillisecondsSinceEpoch(widget.pairing.expiry * 1000);
expiryDate = '${dateTime.year}-'
'${dateTime.month.toString().padLeft(2, '0')}-'
'${dateTime.day.toString().padLeft(2, '0')}';
}
@override
Widget build(BuildContext context) {
return WCCDetailsWidget(
widget.pairing,
expiryDate,
sessionWidgets,
widget.walletKitService,
pairing: widget.pairing,
expiryDate: expiryDate,
sessions: sessions,
walletKitService: widget.walletKitService,
);
}
}
class WCCDetailsWidget extends BasePage {
WCCDetailsWidget(
this.pairing,
this.expiryDate,
this.sessionWidgets,
this.walletKitService,
);
WCCDetailsWidget({
required this.pairing,
required this.expiryDate,
required this.sessions,
required this.walletKitService,
});
final PairingInfo pairing;
final String expiryDate;
final List<Widget> sessionWidgets;
final List<SessionData> sessions;
final WalletKitService walletKitService;
@override
Widget body(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: CircleAvatar(
backgroundImage: (pairing.peerMetadata!.icons.isNotEmpty
? NetworkImage(pairing.peerMetadata!.icons[0])
: AssetImage(
CakeTor.instance!.enabled
? 'assets/images/tor_logo.svg'
: 'assets/images/app_logo.png')) as ImageProvider<Object>,
),
),
const SizedBox(height: 20.0),
Text(
pairing.peerMetadata!.name,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 16.0),
Text(
pairing.peerMetadata!.url,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8.0),
Text(
'${S.current.expiresOn}: $expiryDate',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 20.0),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: sessionWidgets,
),
const SizedBox(height: 20.0),
PrimaryButton(
onPressed: () =>
_onDeleteButtonPressed(context, pairing.peerMetadata!.name, walletKitService),
text: S.current.delete,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
final colors = Theme.of(context).colorScheme;
final metadata = pairing.peerMetadata;
if (metadata == null) {
return const SizedBox.shrink();
}
final iconUrl = metadata.icons.isNotEmpty ? metadata.icons.first : null;
final walletName = getIt.get<AppStore>().wallet?.name ?? '';
return SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
WCDappCard(
name: metadata.name,
iconUrl: iconUrl,
subtitle: metadata.url,
action: WCDappCardAction.connected,
),
const SizedBox(height: 12),
Text(
'${S.of(context).expiresOn}: $expiryDate',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: colors.onSurfaceVariant,
),
),
const SizedBox(height: 24),
for (final session in sessions) ...[
_SessionSection(
session: session,
walletName: walletName,
),
const SizedBox(height: 24),
],
),
const SizedBox(height: 24),
NewPrimaryButton(
onPressed: () => _onDeleteButtonPressed(context, metadata.name, walletKitService),
text: S.current.delete,
color: colors.error,
textColor: colors.onError,
),
const SizedBox(height: 12),
],
),
),
);
@@ -240,3 +163,37 @@ class WCCDetailsWidget extends BasePage {
}
}
}
class _SessionSection extends StatelessWidget {
const _SessionSection({
required this.session,
required this.walletName,
});
final SessionData session;
final String walletName;
String _firstAddress() {
for (final namespace in session.namespaces.values) {
if (namespace.accounts.isNotEmpty) {
return NamespaceUtils.getAccount(namespace.accounts.first);
}
}
return '';
}
@override
Widget build(BuildContext context) {
final permissions = WCPermissionsMapper.fromGeneratedNamespaces(session.namespaces);
final address = _firstAddress();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
WCWalletCard(walletName: walletName, address: address),
const SizedBox(height: 24),
WCPermissionsCard(permissions: permissions),
],
);
}
}
@@ -75,15 +75,15 @@ class EnterWalletConnectURIWidget extends BaseAlertDialog {
fontWeight: FontWeight.w500,
),
suffixIcon: Container(
width: 24,
height: 24,
width: 36,
height: 36,
padding: EdgeInsets.only(top: 0),
child: Semantics(
child: Semantics(
label: S.of(context).paste,
child: InkWell(
onTap: () => _pasteWalletConnectURI(),
child: Container(
padding: EdgeInsets.all(10),
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(6)),
),
@@ -1,94 +0,0 @@
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:flutter/material.dart';
class WCConnectionItemWidget extends StatelessWidget {
const WCConnectionItemWidget({required this.model, Key? key}) : super(key: key);
final WCConnectionModel model;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8),
margin: const EdgeInsetsDirectional.only(top: 8),
child: Visibility(
visible: model.elements != null,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
model.title ?? '',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
if (model.elements != null)
Wrap(
spacing: 4,
runSpacing: 4,
direction: Axis.horizontal,
children: model.elements!
.map((e) => _ModelElementWidget(model: model, modelElement: e))
.toList(),
),
],
),
replacement: _NoModelElementWidget(model: model),
),
);
}
}
class _NoModelElementWidget extends StatelessWidget {
const _NoModelElementWidget({required this.model});
final WCConnectionModel model;
@override
Widget build(BuildContext context) {
return Text(
model.text!,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w600,
),
);
}
}
class _ModelElementWidget extends StatelessWidget {
const _ModelElementWidget({
required this.model,
required this.modelElement,
});
final WCConnectionModel model;
final String modelElement;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: model.elementActions != null ? model.elementActions![modelElement] : null,
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(6),
),
padding: const EdgeInsets.all(8),
child: Text(
modelElement,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 50,
overflow: TextOverflow.ellipsis,
),
),
);
}
}
@@ -0,0 +1,94 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/confirm_swiper.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';
import 'package:cake_wallet/src/screens/wallet_connect/utils/wc_permissions_mapper.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_dapp_card.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_permissions_card.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_scam_banner.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_sheet_header.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_wallet_card.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCConnectionRequestSheet extends StatelessWidget {
const WCConnectionRequestSheet({
super.key,
required this.proposalData,
required this.requester,
required this.walletKeyService,
required this.appStore,
this.verifyContext,
});
final ProposalData proposalData;
final ConnectionMetadata requester;
final WalletConnectKeyService walletKeyService;
final AppStore appStore;
final VerifyContext? verifyContext;
String _resolveAddress() {
final wallet = appStore.wallet;
if (wallet == null) return '';
final keys = walletKeyService.getKeysForChain(wallet);
if (keys.isEmpty) return '';
return keys.first.publicKey;
}
@override
Widget build(BuildContext context) {
final permissions =
WCPermissionsMapper.fromGeneratedNamespaces(proposalData.generatedNamespaces ?? const {});
final metadata = requester.metadata;
final iconUrl = metadata.icons.isNotEmpty ? metadata.icons.first : null;
final walletName = appStore.wallet?.name ?? '';
final address = _resolveAddress();
final isScam = verifyContext?.validation.scam ?? false;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
WCSheetHeader(title: S.of(context).wc_connect_request_title),
const SizedBox(height: 24),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
WCDappCard(
name: metadata.name,
iconUrl: iconUrl,
subtitle: metadata.url,
action: WCDappCardAction.connect,
verifyContext: verifyContext,
),
if (isScam) ...[
const SizedBox(height: 16),
const WCScamBanner(),
],
const SizedBox(height: 24),
WCWalletCard(walletName: walletName, address: address),
const SizedBox(height: 24),
WCPermissionsCard(permissions: permissions),
],
),
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: ConfirmSwiper(
swiperText: S.of(context).wc_swipe_to_approve,
onConfirmed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.one);
}
},
),
),
const SizedBox(height: 16),
],
);
}
}
@@ -1,100 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:cake_wallet/src/screens/wallet_connect/utils/namespace_model_builder.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_connection_widget.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCConnectionRequestWidget extends StatelessWidget {
WCConnectionRequestWidget({
this.sessionAuthPayload,
this.proposalData,
this.requester,
this.verifyContext,
required this.walletKeyService,
required this.walletKit,
required this.appStore,
});
final SessionAuthPayload? sessionAuthPayload;
final ProposalData? proposalData;
final ConnectionMetadata? requester;
final VerifyContext? verifyContext;
final WalletConnectKeyService walletKeyService;
final AppStore appStore;
final ReownWalletKit walletKit;
@override
Widget build(BuildContext context) {
if (requester == null) {
return Text(S.current.error.toUpperCase());
}
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 8),
Text(
'${requester!.metadata.name} ${S.current.wouoldLikeToConnect}',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 18.0,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
(sessionAuthPayload != null)
? _buildSessionAuthRequestView()
: _buildSessionProposalView(context),
],
),
);
}
Widget _buildSessionAuthRequestView() {
final cacaoPayload = CacaoRequestPayload.fromSessionAuthPayload(
sessionAuthPayload!,
);
final List<WCConnectionModel> messagesModels = [];
for (var chain in sessionAuthPayload!.chains) {
final chainKeys = walletKeyService.getKeysForChain(appStore.wallet!);
final iss = 'did:pkh:$chain:${chainKeys.first.publicKey}';
final message = walletKit.formatAuthMessage(
iss: iss,
cacaoPayload: cacaoPayload,
);
messagesModels.add(
WCConnectionModel(
title: '${S.current.message} ${messagesModels.length + 1}',
elements: [message],
),
);
}
return WCConnectionWidget(
title: '${messagesModels.length} ${S.current.messages}',
info: messagesModels,
);
}
Widget _buildSessionProposalView(BuildContext context) {
// Create the connection models using the required and optional namespaces provided by the proposal data
final views = ConnectionWidgetBuilder.buildFromRequiredNamespaces(
proposalData!.generatedNamespaces ?? {},
);
return Column(children: views);
}
}
@@ -1,45 +0,0 @@
import 'package:cake_wallet/src/screens/wallet_connect/models/wc_connection_model.dart';
import 'package:flutter/material.dart';
import 'wc_connection_item_widget.dart';
class WCConnectionWidget extends StatelessWidget {
const WCConnectionWidget({required this.title, required this.info, super.key});
final String title;
final List<WCConnectionModel> info;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(8),
),
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 8),
child: Text(
title,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
const SizedBox(height: 8),
...info.map((e) => WCConnectionItemWidget(model: e)),
],
),
);
}
}
@@ -0,0 +1,132 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/themes/core/custom_theme_colors.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
enum WCDappCardAction { connect, sign, connected }
class WCDappCard extends StatelessWidget {
const WCDappCard({
super.key,
required this.name,
required this.iconUrl,
required this.subtitle,
required this.action,
this.verifyContext,
});
final String name;
final String? iconUrl;
final String subtitle;
final WCDappCardAction action;
final VerifyContext? verifyContext;
String _actionLine(BuildContext context) {
switch (action) {
case WCDappCardAction.connect:
return S.of(context).wc_would_like_to_connect_to(name);
case WCDappCardAction.sign:
return S.of(context).wc_would_like_to_sign(name);
case WCDappCardAction.connected:
return S.of(context).wc_connected_to(name);
}
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 56,
height: 56,
child: CakeImageWidget(
borderRadius: 16,
imageUrl: iconUrl,
fit: BoxFit.cover,
errorWidget: CakeImageWidget(
imageUrl: 'assets/new-ui/walletconnect_icon.svg',
width: 40,
height: 40,
),
),
),
const SizedBox(height: 10),
Text(
_actionLine(context),
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
if (subtitle.trim().isNotEmpty) ...[
const SizedBox(height: 10),
Text(
subtitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: colors.onSurfaceVariant,
),
),
],
if (_shouldShowBadge()) ...[
const SizedBox(height: 10),
_VerifyBadge(verifyContext: verifyContext!),
],
],
),
);
}
bool _shouldShowBadge() {
if (verifyContext == null) return false;
if (verifyContext!.validation.scam) return false;
return true;
}
}
class _VerifyBadge extends StatelessWidget {
const _VerifyBadge({required this.verifyContext});
final VerifyContext verifyContext;
@override
Widget build(BuildContext context) {
final IconData icon;
final Color color;
final String label;
if (verifyContext.validation.valid) {
icon = Icons.check_circle;
color = CustomThemeColors.syncGreen;
label = S.of(context).wc_verified;
} else {
icon = Icons.warning_amber_rounded;
color = CustomThemeColors.syncYellow;
label = S.of(context).wc_not_verified;
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 4),
Text(
label,
style: Theme.of(context).textTheme.bodySmall!.copyWith(color: color),
),
],
);
}
}
@@ -0,0 +1,51 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
class WCHeroCard extends StatelessWidget {
const WCHeroCard();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colors = theme.colorScheme;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: CakeImageWidget(
imageUrl: 'assets/new-ui/walletconnect_icon.svg',
width: 36,
height: 36,
fit: BoxFit.cover,
),
),
const SizedBox(height: 10),
Text(
S.of(context).walletConnect,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w500,
letterSpacing: -0.08,
),
),
const SizedBox(height: 10),
Text(
S.of(context).wc_pairing_list_header_subtitle,
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
letterSpacing: -0.06,
color: colors.onSurfaceVariant,
),
),
],
),
);
}
}
@@ -0,0 +1,86 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
class WCMessageRow {
const WCMessageRow({required this.label, required this.value});
final String label;
final String value;
}
class WCMessageCard extends StatelessWidget {
const WCMessageCard({
super.key,
required this.message,
this.title,
this.extraRows = const [],
});
final String? title;
final String message;
final List<WCMessageRow> extraRows;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final resolvedTitle = title ?? S.of(context).wc_message_to_sign;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
resolvedTitle,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w600,
color: colors.onSurface,
),
),
if (message.isNotEmpty) ...[
const SizedBox(height: 8),
SelectableText(
message,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: colors.onSurfaceVariant,
height: 1.4,
),
),
],
for (final row in extraRows) ...[
const SizedBox(height: 12),
Divider(height: 1, color: colors.outlineVariant),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
row.label,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: colors.onSurface,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
row.value,
textAlign: TextAlign.end,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: colors.onSurfaceVariant,
),
),
),
],
),
],
],
),
);
}
}
@@ -1,89 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCPairingItemWidget extends StatelessWidget {
const WCPairingItemWidget({required this.pairing, required this.onTap, super.key});
final PairingInfo pairing;
final void Function() onTap;
@override
Widget build(BuildContext context) {
PairingMetadata? metadata = pairing.peerMetadata;
if (metadata == null) {
return SizedBox.shrink();
}
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(pairing.expiry * 1000);
int year = dateTime.year;
int month = dateTime.month;
int day = dateTime.day;
String expiryDate =
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
return ListTile(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: SizedBox(
width: 60,
height: 60,
child: CakeImageWidget(
borderRadius: 8,
width: 60,
height: 60,
imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0] : null,
errorWidget: CircleAvatar(
backgroundImage: AssetImage('assets/images/walletconnect_logo.png'),
),
),
),
title: Text(
metadata.name,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 16.0,
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
metadata.url,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
Text(
'${S.current.expiresOn}: $expiryDate',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
trailing: SizedBox(
width: 44,
height: 40,
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: Icon(
Icons.edit,
size: 14,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
onTap: onTap,
);
}
}
@@ -0,0 +1,64 @@
import 'package:cake_wallet/src/screens/wallet_connect/utils/wc_permissions_mapper.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:flutter/material.dart';
class WCPermissionsCard extends StatelessWidget {
const WCPermissionsCard({super.key, required this.permissions});
final List<WCPermission> permissions;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
if (permissions.isEmpty) return const SizedBox.shrink();
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
for (int i = 0; i < permissions.length; i++) ...[
if (i > 0) Divider(height: 1, color: colors.outlineVariant, indent: 44),
_PermissionRow(permission: permissions[i]),
],
],
),
);
}
}
class _PermissionRow extends StatelessWidget {
const _PermissionRow({required this.permission});
final WCPermission permission;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 14),
child: Row(
children: [
Container(
child: CakeImageWidget(
imageUrl: permission.iconUrl,
width: 28,
height: 28,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
permission.label,
style: Theme.of(context).textTheme.bodyMedium,
),
),
],
),
);
}
}
@@ -1,69 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_verify_context_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCRequestWidget extends StatelessWidget {
WCRequestWidget({
required this.child,
this.verifyContext,
this.onAccept,
this.onReject,
});
final Widget child;
final VerifyContext? verifyContext;
final VoidCallback? onAccept;
final VoidCallback? onReject;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
WCVerifyContextWidget(
verifyContext: verifyContext,
),
const SizedBox(height: 8),
Flexible(
child: SingleChildScrollView(child: child),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: PrimaryButton(
onPressed: onReject ??
() {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.reject);
}
},
text: S.current.reject,
color: Theme.of(context).colorScheme.error,
textColor: Theme.of(context).colorScheme.onError,
),
),
const SizedBox(width: 16),
Expanded(
child: PrimaryButton(
onPressed: onAccept ??
() {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.one);
}
},
text: S.current.approve,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
],
),
],
);
}
}
@@ -0,0 +1,56 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
class WCScamBanner extends StatelessWidget {
const WCScamBanner({super.key});
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.errorContainer,
border: Border.all(color: colors.onError, width: 2),
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.warning_amber_rounded,
size: 36,
color: colors.error,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).wc_scam_warning_title,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: colors.error,
letterSpacing: -0.06,
),
),
const SizedBox(height: 4),
Text(
S.of(context).wc_scam_warning_message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: colors.error,
),
),
],
),
),
],
),
);
}
}
@@ -1,60 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:flutter/material.dart';
class WCSessionAuthRequestWidget extends StatelessWidget {
const WCSessionAuthRequestWidget({super.key, required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: SingleChildScrollView(child: child),
),
const SizedBox(height: 16),
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
PrimaryButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.reject);
}
},
text: S.current.cancel,
color: Theme.of(context).colorScheme.error,
textColor: Theme.of(context).colorScheme.onError,
),
const SizedBox(height: 8),
PrimaryButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.one);
}
},
text: S.current.sign_one,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
const SizedBox(height: 8),
PrimaryButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.all);
}
},
text: S.current.sign_all,
color: Theme.of(context).colorScheme.secondaryContainer,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
),
],
),
],
);
}
}
@@ -0,0 +1,50 @@
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:flutter/material.dart';
class WCSheetHeader extends StatelessWidget {
const WCSheetHeader({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return SizedBox(
height: 40,
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: InkResponse(
radius: 36,
onTap: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.reject);
}
},
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: colors.surfaceContainerHighest,
shape: BoxShape.circle,
),
child: Icon(
Icons.close,
size: 20,
color: colors.onSurface,
),
),
),
),
Text(
title,
style: Theme.of(context).textTheme.headlineMedium!.copyWith(letterSpacing: -0.09),
),
],
),
);
}
}
@@ -0,0 +1,111 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/new-ui/widgets/confirm_swiper.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_dapp_card.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_message_card.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_scam_banner.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_sheet_header.dart';
import 'package:cake_wallet/src/screens/wallet_connect/widgets/wc_wallet_card.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCSigningRequestSheet extends StatelessWidget {
const WCSigningRequestSheet({
super.key,
required this.title,
required this.swipeLabel,
required this.dappName,
required this.message,
required this.walletName,
required this.address,
this.dappIconUrl,
this.dappSubtitle,
this.verifyContext,
this.messageTitle,
this.extraRows = const [],
this.signAllCount,
});
final String title;
final String swipeLabel;
final String dappName;
final String? dappIconUrl;
final String? dappSubtitle;
final String message;
final String? messageTitle;
final List<WCMessageRow> extraRows;
final String walletName;
final String address;
final VerifyContext? verifyContext;
final int? signAllCount;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
final showSignAll = (signAllCount ?? 0) > 1;
final isScam = verifyContext?.validation.scam ?? false;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
WCSheetHeader(title: title),
const SizedBox(height: 24),
Expanded(
child: SingleChildScrollView(
child: Column(
children: [
WCDappCard(
name: dappName,
iconUrl: dappIconUrl,
subtitle: dappSubtitle ?? '',
action: WCDappCardAction.sign,
verifyContext: verifyContext,
),
if (isScam) ...[
const SizedBox(height: 16),
const WCScamBanner(),
],
const SizedBox(height: 24),
WCWalletCard(walletName: walletName, address: address),
const SizedBox(height: 24),
WCMessageCard(
title: messageTitle,
message: message,
extraRows: extraRows,
),
],
),
),
),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: ConfirmSwiper(
swiperText: swipeLabel,
onConfirmed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.one);
}
},
),
),
if (showSignAll) ...[
const SizedBox(height: 24),
TextButton(
onPressed: () {
if (Navigator.canPop(context)) {
Navigator.of(context).pop(WCBottomSheetResult.all);
}
},
style: TextButton.styleFrom(
foregroundColor: colors.primary,
minimumSize: const Size.fromHeight(48),
),
child: Text(S.of(context).wc_sign_all_count(signAllCount!.toString())),
),
],
const SizedBox(height: 16),
],
);
}
}
@@ -1,131 +0,0 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/core/custom_theme_colors.dart';
import 'package:flutter/material.dart';
import 'package:reown_walletkit/reown_walletkit.dart';
class WCVerifyContextWidget extends StatelessWidget {
const WCVerifyContextWidget({
super.key,
required this.verifyContext,
});
final VerifyContext? verifyContext;
@override
Widget build(BuildContext context) {
if (verifyContext == null) {
return const SizedBox.shrink();
}
if (verifyContext!.validation.scam) {
return VerifyBanner(
color: Theme.of(context).colorScheme.errorContainer,
origin: verifyContext!.origin,
title: S.current.security_risk,
text: S.current.security_risk_description,
);
}
if (verifyContext!.validation.invalid) {
return VerifyBanner(
color: Theme.of(context).colorScheme.errorContainer,
origin: verifyContext!.origin,
title: S.current.domain_mismatch,
text: S.current.domain_mismatch_description,
);
}
if (verifyContext!.validation.valid) {
return VerifyHeader(
iconColor: Theme.of(context).colorScheme.onPrimary,
title: verifyContext!.origin,
);
}
return VerifyBanner(
color: CustomThemeColors.syncYellow,
origin: verifyContext!.origin,
title: S.current.cannot_verify,
text: S.current.cannot_verify_description,
);
}
}
class VerifyHeader extends StatelessWidget {
const VerifyHeader({
super.key,
required this.iconColor,
required this.title,
});
final Color iconColor;
final String title;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.shield_outlined,
color: iconColor,
),
const SizedBox(width: 8),
Text(
title,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: iconColor,
fontWeight: FontWeight.bold,
),
),
],
);
}
}
class VerifyBanner extends StatelessWidget {
const VerifyBanner({
super.key,
required this.origin,
required this.title,
required this.text,
required this.color,
});
final String origin, title, text;
final Color color;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
origin,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox.square(dimension: 8.0),
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: color.withOpacity(0.2),
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
),
child: Column(
children: [
VerifyHeader(
iconColor: color,
title: title,
),
const SizedBox(height: 4.0),
Text(
text,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
);
}
}
@@ -0,0 +1,95 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/utils/clipboard_util.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class WCWalletCard extends StatelessWidget {
const WCWalletCard({
super.key,
required this.walletName,
required this.address,
});
final String walletName;
final String address;
static String _truncate(String value) {
if (value.length <= 14) return value;
return '${value.substring(0, 6)}...${value.substring(value.length - 7)}';
}
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: colors.surfaceContainer,
borderRadius: BorderRadius.circular(20),
),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
WalletDetailEntry(label: S.of(context).wallet, value: walletName),
Divider(height: 1, color: colors.outlineVariant),
WalletDetailEntry(
label: S.of(context).address,
value: _truncate(address),
onTap: address.isEmpty
? null
: () async {
await ClipboardUtil.setSensitiveDataToClipboard(
ClipboardData(text: address),
);
if (!context.mounted) return;
showBar<void>(context, S.of(context).copied_to_clipboard);
},
),
],
),
);
}
}
class WalletDetailEntry extends StatelessWidget {
const WalletDetailEntry({required this.label, required this.value, this.onTap});
final String label;
final String value;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w500,
),
),
Flexible(
child: Text(
value,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: colors.onSurfaceVariant,
),
),
),
],
),
),
);
}
}
+7 -9
View File
@@ -1,6 +1,4 @@
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:vector_graphics/vector_graphics.dart';
@@ -15,7 +13,9 @@ class CakeImageWidget extends StatelessWidget {
this.errorWidget,
this.color,
this.colorFilter,
this.borderRadius = 24.0, this.alignment, this.allowDrawingOutsideViewBox,
this.borderRadius = 24.0,
this.alignment,
this.allowDrawingOutsideViewBox,
});
final String? imageUrl;
@@ -38,8 +38,8 @@ class CakeImageWidget extends StatelessWidget {
final isSvg = imageUrl!.toLowerCase().endsWith('.svg');
final isAsset = imageUrl!.startsWith('assets/');
final effectiveColorFilter = colorFilter ??
(color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null);
final effectiveColorFilter =
colorFilter ?? (color != null ? ColorFilter.mode(color!, BlendMode.srcIn) : null);
Widget imageWidget;
if (isAsset) {
@@ -49,8 +49,7 @@ class CakeImageWidget extends StatelessWidget {
width: width,
alignment: alignment ?? Alignment.center,
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
colorFilter:
effectiveColorFilter,
colorFilter: effectiveColorFilter,
fit: fit ?? BoxFit.contain, errorBuilder: (context, e, trace) {
return SvgPicture.asset(
imageUrl!,
@@ -59,8 +58,7 @@ class CakeImageWidget extends StatelessWidget {
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox ?? false,
width: width,
errorBuilder: (_, __, ___) => SizedBox(height: height, width: width),
colorFilter:
effectiveColorFilter,
colorFilter: effectiveColorFilter,
fit: fit ?? BoxFit.contain,
);
});
@@ -23,7 +23,9 @@ class ListItemRegularRowWidget extends StatelessWidget {
this.trailingIconSize,
this.bottomWidget,
this.trailingWidget,
this.copyableText
this.copyableText,
this.leadingIconErrorWidget,
this.leadingIconSize,
});
final String keyValue;
@@ -42,6 +44,8 @@ class ListItemRegularRowWidget extends StatelessWidget {
final Color? foregroundColor;
final double? trailingIconSize;
final String? copyableText;
final Widget? leadingIconErrorWidget;
final double? leadingIconSize;
@override
Widget build(BuildContext context) {
@@ -78,8 +82,9 @@ class ListItemRegularRowWidget extends StatelessWidget {
padding: const EdgeInsets.only(right: 12.0),
child: CakeImageWidget(
imageUrl: iconPath!,
width: 24,
height: 24,
width: leadingIconSize ?? 24,
height: leadingIconSize ?? 24,
errorWidget: leadingIconErrorWidget,
)),
Flexible(
child: Column(
@@ -107,6 +107,8 @@ class NewListSections extends StatelessWidget {
trailingIconSize: item.trailingIconSize,
trailingWidget: item.trailingWidget,
bottomWidget: item.bottomWidget,
leadingIconErrorWidget: item.leadingIconErrorWidget,
leadingIconSize: item.leadingIconSize,
);
}