CW-1094-WalletConnect-Issues (#2318)

* feat(walletconnect): Minor update to WalletConnect tile UI to fix expanded image issue

* feat(walletconnect): Minor update to WalletConnect tile UI to fix expanded image issue

* feat(walletconnect): Enhance WalletConnect EVM chain service.

This change:
- Improves signTypedDataV4 method handing and data parsing in extractPermitData.
- Adjusts UI for One Click Auth requests

* feat(walletconnect): Add redirect to PairingMetadata in WalletKit setup

* fix(walletconnect): Ensure session null checks before handling redirects in EvmChainService

* fix(walletconnect): Add null safety checks for permitData properties in EvmChainService

* refactor(walletconnect): Update WCPairingItemWidget layout and improve error handling for image loading

* fix(walletconnect): Handle break in connection flow triggered by global exception handler when SVGParser fails on invalid SvgData and triggers FlutterError.

* refactor(solana): Remove redundant session request responses and simplify error handling in SolanaChainService

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke 2025-06-24 03:48:27 +01:00 committed by GitHub
parent af89603b81
commit 65bb917bfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 141 additions and 97 deletions

View file

@ -379,16 +379,21 @@ class EvmChainServiceImpl {
topic: topic,
response: response,
);
if (session == null) return;
MethodsUtils.handleRedirect(
topic,
session!.peer.metadata.redirect,
session.peer.metadata.redirect,
response.error?.message,
response.error == null,
);
} on ReownSignError catch (error) {
if (session == null) return;
MethodsUtils.handleRedirect(
topic,
session!.peer.metadata.redirect,
session.peer.metadata.redirect,
error.message,
);
}
@ -489,68 +494,76 @@ class EvmChainServiceImpl {
final typedData = jsonDecode(data[1] as String) as Map<String, dynamic>;
// Extracting domain details.
final domain = typedData['domain'] ?? {} as Map<String, dynamic>;
final domain = typedData['domain'] as Map<String, dynamic>? ?? {};
final domainName = domain['name']?.toString() ?? '';
final version = domain['version']?.toString() ?? '';
final chainId = domain['chainId']?.toString() ?? '';
final verifyingContract = domain['verifyingContract']?.toString() ?? '';
final chainId = domain['chainId']?.toString() ?? '';
final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type);
// Get the primary type.
// Get the primary type and types
final primaryType = typedData['primaryType']?.toString() ?? '';
final types = typedData['types'] as Map<String, dynamic>? ?? {};
final message = typedData['message'] as Map<String, dynamic>? ?? {};
// Extracting message details.
final message = typedData['message'] ?? {} as Map<String, dynamic>;
final details = message['details'] ?? {} as Map<String, dynamic>;
final amount = details['amount']?.toString() ?? '';
final expirationRaw = details['expiration']?.toString() ?? '';
final nonce = details['nonce']?.toString() ?? '';
// Build a readable message based on the primary type and its structure
String messageDetails = '';
final tokenAddress = details['token']?.toString() ?? '';
final token = await getTokenDetails(tokenAddress, chainName);
final spender = message['spender']?.toString() ?? '';
final sigDeadlineRaw = message['sigDeadline']?.toString() ?? '';
// Converting expiration and sigDeadline from Unix time (seconds) to DateTime.
DateTime? expirationDate;
DateTime? sigDeadlineDate;
try {
if (expirationRaw.isNotEmpty) {
final int expirationInt = int.parse(expirationRaw);
expirationDate = DateTime.fromMillisecondsSinceEpoch(expirationInt * 1000);
}
if (sigDeadlineRaw.isNotEmpty) {
final int sigDeadlineInt = int.parse(sigDeadlineRaw);
sigDeadlineDate = DateTime.fromMillisecondsSinceEpoch(sigDeadlineInt * 1000);
}
} catch (e) {
// Parsing failed; we leave dates as null.
if (types.containsKey(primaryType)) {
final typeFields = types[primaryType] as List<dynamic>;
messageDetails = _formatMessageFields(message, typeFields, types);
} else {
// For unknown types, show the raw message
messageDetails = message.toString();
}
final permitData = {
'domainName': domainName,
'chainId': chainId,
'verifyingContract': verifyingContract,
'primaryType': primaryType,
'token': token,
'amount': amount,
'expiration': expirationDate,
'nonce': nonce,
'spender': spender,
'sigDeadline': sigDeadlineDate,
};
return 'Domain: ${permitData['domainName']}'
'Chain ID: ${permitData['chainId']}'
'Verifying Contract: ${permitData['verifyingContract']}'
'Primary Type: ${permitData['primaryType']}'
'Token: ${permitData['token']}'
'Expiration: ${permitData['expiration'] != null ? permitData['expiration'] : 'N/A'}'
'Spender: ${permitData['spender']}'
'Signature Deadline: ${permitData['sigDeadline'] != null ? permitData['sigDeadline'] : 'N/A'}';
return '''Domain Name: $domainName
Version: $version
Chain ID: $chainId
Verifying Contract: $verifyingContract
Primary Type: $primaryType\n
Message:
$messageDetails''';
}
return '';
return 'Invalid typed data format';
}
String _formatMessageFields(
Map<String, dynamic> message, List<dynamic> fields, Map<String, dynamic> types) {
final buffer = StringBuffer();
for (var field in fields) {
final fieldName = _toCamelCase(field['name'] as String);
final fieldType = field['type'] as String;
final value = message[field['name'] as String];
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
final name = value['name'] as String;
final wallet = value['wallet'] as String;
buffer.writeln('$fieldName: $name ($wallet)');
} else {
// For other nested types, format each field
final formattedValue =
_formatMessageFields(value as Map<String, dynamic>, nestedFields, types);
buffer.writeln('$fieldName: $formattedValue');
}
} else {
// Handle primitive types
buffer.writeln('$fieldName: $value');
}
}
return buffer.toString();
}
String _toCamelCase(String input) {
if (input.isEmpty) return input;
return input[0].toUpperCase() + input.substring(1).toLowerCase();
}
Future<String> getTokenDetails(String contractAddress, String chainName) async {

View file

@ -2,7 +2,6 @@ import 'dart:convert';
import 'package:blockchain_utils/base58/base58.dart';
import 'package:blockchain_utils/blockchain_utils.dart' as blockchain_utils;
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart';
import 'package:flutter/material.dart';
import 'package:on_chain/solana/solana.dart';
@ -91,8 +90,6 @@ class SolanaChainService {
);
}
await walletKit.respondSessionRequest(topic: topic, response: response);
_handleResponseForTopic(topic, response);
}
@ -158,8 +155,6 @@ class SolanaChainService {
);
}
await walletKit.respondSessionRequest(topic: topic, response: response);
_handleResponseForTopic(topic, response);
}
@ -221,8 +216,6 @@ class SolanaChainService {
);
}
await walletKit.respondSessionRequest(topic: topic, response: response);
_handleResponseForTopic(topic, response);
}
@ -242,15 +235,9 @@ class SolanaChainService {
topic,
session!.peer.metadata.redirect,
response.error?.message,
response.error == null,
);
} on ReownSignError catch (error) {
if (error.message.contains('No matching key')) {
MethodsUtils.handleRedirect(
topic,
session!.peer.metadata.redirect,
'${S.current.error_while_processing} ${S.current.youCanGoBackToYourDapp}',
);
} else {
MethodsUtils.handleRedirect(
topic,
session!.peer.metadata.redirect,
@ -259,7 +246,6 @@ class SolanaChainService {
}
}
}
}
extension on Map<String, dynamic> {
TransactionInstruction toInstruction() {

View file

@ -78,6 +78,7 @@ abstract class WalletKitServiceBase with Store {
description: 'Cake Wallet',
url: 'https://cakewallet.com',
icons: ['https://cakewallet.com/assets/image/cake_logo.png'],
redirect: Redirect(native: 'cakewallet://'),
),
);

View file

@ -85,7 +85,7 @@ class _ModelElementWidget extends StatelessWidget {
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 10,
maxLines: 50,
overflow: TextOverflow.ellipsis,
),
),

View file

@ -26,12 +26,20 @@ class WCPairingItemWidget extends StatelessWidget {
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
return ListTile(
leading: CakeImageWidget(
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(
@ -59,9 +67,10 @@ class WCPairingItemWidget extends StatelessWidget {
),
],
),
trailing: Container(
height: 40,
trailing: SizedBox(
width: 44,
height: 40,
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
shape: BoxShape.circle,
@ -73,6 +82,7 @@ class WCPairingItemWidget extends StatelessWidget {
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
onTap: onTap,
);
}

View file

@ -17,7 +17,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
child: SingleChildScrollView(child: child),
),
const SizedBox(height: 16),
Row(
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
PrimaryButton(
@ -30,7 +30,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
color: Theme.of(context).colorScheme.error,
textColor: Theme.of(context).colorScheme.onError,
),
const SizedBox(width: 8),
const SizedBox(height: 8),
PrimaryButton(
onPressed: () {
if (Navigator.canPop(context)) {
@ -41,7 +41,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
const SizedBox(width: 8),
const SizedBox(height: 8),
PrimaryButton(
onPressed: () {
if (Navigator.canPop(context)) {
@ -49,8 +49,8 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
}
},
text: S.current.sign_all,
color: Theme.of(context).secondaryHeaderColor,
textColor: Theme.of(context).colorScheme.onPrimary,
color: Theme.of(context).colorScheme.secondaryContainer,
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
),
],
),

View file

@ -11,6 +11,7 @@ class CakeImageWidget extends StatelessWidget {
this.loadingWidget,
this.errorWidget,
this.color,
this.borderRadius = 24.0,
});
final String? imageUrl;
@ -20,6 +21,7 @@ class CakeImageWidget extends StatelessWidget {
final Widget? loadingWidget;
final Widget? errorWidget;
final Color? color;
final double borderRadius;
@override
Widget build(BuildContext context) {
@ -80,7 +82,7 @@ class CakeImageWidget extends StatelessWidget {
height: height,
width: width,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24.0),
borderRadius: BorderRadius.circular(borderRadius),
color: Theme.of(context).colorScheme.surfaceContainerHighest,
),
child: Center(

View file

@ -289,6 +289,15 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo
child: CircularProgressIndicator(),
),
),
errorBuilder: (_, __, ___) {
return Container(
height: imageHeight,
width: imageWidth,
child: Center(
child: Icon(Icons.error_outline, color: Colors.grey),
),
);
},
)
: Image.network(
imagePath,
@ -315,6 +324,9 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo
return Container(
height: imageHeight,
width: imageWidth,
child: Center(
child: Icon(Icons.error_outline, color: Colors.grey),
),
);
},
);
@ -325,6 +337,7 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo
height: imageHeight,
width: imageWidth,
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
errorBuilder: (_, __, ___) => Icon(Icons.error, color: Colors.grey),
)
: Image.asset(imagePath, height: imageHeight, width: imageWidth);
}

View file

@ -274,6 +274,12 @@ class ExceptionHandler {
"NetworkImage._loadAsync",
"SSLV3_ALERT_BAD_RECORD_MAC",
"PlatformException(already_active, File picker is already active",
// SVG-related errors
"SvgParser",
"SVG parsing error",
"Invalid SVG",
"SVG format error",
"SvgPicture",
// Temporary ignored, More context: Flutter secure storage reads the values as null some times
// probably when the device was locked and then opened on Cake
// this is solved by a restart of the app

View file

@ -26,6 +26,15 @@ class ImageUtil {
child: CircularProgressIndicator(),
),
),
errorBuilder: (_, __, ___) {
return Container(
height: _height,
width: _width,
child: Center(
child: Icon(Icons.error_outline, color: Colors.grey),
),
);
},
)
: Image.network(
key: ValueKey(imagePath),
@ -54,6 +63,9 @@ class ImageUtil {
return Container(
height: _height,
width: _width,
child: Center(
child: Icon(Icons.error_outline, color: Colors.grey),
),
);
},
);
@ -64,6 +76,7 @@ class ImageUtil {
height: _height,
width: _width,
placeholderBuilder: (_) => Icon(Icons.error),
errorBuilder: (_, __, ___) => Icon(Icons.error),
key: ValueKey(imagePath),
)
: Image.asset(