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