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>

David Adegoke committed Jun 24, 2025 at 03:48 UTC 65bb917bfb805ef5066055bb7a057a21a6105e8d
10 files changed +140 -96
lib/src/screens/wallet_connect/services/chain_service/eth/evm_chain_service.dart
+67 -54
@@ -379,16 +379,21 @@ class EvmChainServiceImpl {
379 topic: topic,
380 response: response,
381 );
382 +
383 + if (session == null) return;
384 +
385 MethodsUtils.handleRedirect(
386 topic,
384 - session!.peer.metadata.redirect,
387 + session.peer.metadata.redirect,
388 response.error?.message,
389 response.error == null,
390 );
391 } on ReownSignError catch (error) {
392 + if (session == null) return;
393 +
394 MethodsUtils.handleRedirect(
395 topic,
391 - session!.peer.metadata.redirect,
396 + session.peer.metadata.redirect,
397 error.message,
398 );
399 }
@@ -489,68 +494,76 @@ class EvmChainServiceImpl {
494 final typedData = jsonDecode(data[1] as String) as Map<String, dynamic>;
495
496 // Extracting domain details.
492 - final domain = typedData['domain'] ?? {} as Map<String, dynamic>;
497 + final domain = typedData['domain'] as Map<String, dynamic>? ?? {};
498 final domainName = domain['name']?.toString() ?? '';
494 - final verifyingContract = domain['verifyingContract']?.toString() ?? '';
495 -
499 + final version = domain['version']?.toString() ?? '';
500 final chainId = domain['chainId']?.toString() ?? '';
497 - final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type);
501 + final verifyingContract = domain['verifyingContract']?.toString() ?? '';
502
499 - // Get the primary type.
503 + // Get the primary type and types
504 final primaryType = typedData['primaryType']?.toString() ?? '';
505 + final types = typedData['types'] as Map<String, dynamic>? ?? {};
506 + final message = typedData['message'] as Map<String, dynamic>? ?? {};
507 +
508 + // Build a readable message based on the primary type and its structure
509 + String messageDetails = '';
510 +
511 + if (types.containsKey(primaryType)) {
512 + final typeFields = types[primaryType] as List<dynamic>;
513 + messageDetails = _formatMessageFields(message, typeFields, types);
514 + } else {
515 + // For unknown types, show the raw message
516 + messageDetails = message.toString();
517 + }
518
502 - // Extracting message details.
503 - final message = typedData['message'] ?? {} as Map<String, dynamic>;
504 - final details = message['details'] ?? {} as Map<String, dynamic>;
505 - final amount = details['amount']?.toString() ?? '';
506 - final expirationRaw = details['expiration']?.toString() ?? '';
507 - final nonce = details['nonce']?.toString() ?? '';
508 -
509 - final tokenAddress = details['token']?.toString() ?? '';
510 - final token = await getTokenDetails(tokenAddress, chainName);
511 -
512 - final spender = message['spender']?.toString() ?? '';
513 - final sigDeadlineRaw = message['sigDeadline']?.toString() ?? '';
519 + return '''Domain Name: $domainName
520 +Version: $version
521 +Chain ID: $chainId
522 +Verifying Contract: $verifyingContract
523 +Primary Type: $primaryType\n
524 +Message:
525 +$messageDetails''';
526 + }
527 + return 'Invalid typed data format';
528 + }
529
515 - // Converting expiration and sigDeadline from Unix time (seconds) to DateTime.
516 - DateTime? expirationDate;
517 - DateTime? sigDeadlineDate;
518 - try {
519 - if (expirationRaw.isNotEmpty) {
520 - final int expirationInt = int.parse(expirationRaw);
521 - expirationDate = DateTime.fromMillisecondsSinceEpoch(expirationInt * 1000);
530 + String _formatMessageFields(
531 + Map<String, dynamic> message, List<dynamic> fields, Map<String, dynamic> types) {
532 + final buffer = StringBuffer();
533 +
534 + for (var field in fields) {
535 + final fieldName = _toCamelCase(field['name'] as String);
536 + final fieldType = field['type'] as String;
537 + final value = message[field['name'] as String];
538 +
539 + if (value == null) continue;
540 +
541 + if (types.containsKey(fieldType)) {
542 + // Handle nested types
543 + final nestedFields = types[fieldType] as List<dynamic>;
544 + if (fieldType == 'Person') {
545 + // Special formatting for Person type
546 + final name = value['name'] as String;
547 + final wallet = value['wallet'] as String;
548 + buffer.writeln('$fieldName: $name ($wallet)');
549 + } else {
550 + // For other nested types, format each field
551 + final formattedValue =
552 + _formatMessageFields(value as Map<String, dynamic>, nestedFields, types);
553 + buffer.writeln('$fieldName: $formattedValue');
554 }
523 - if (sigDeadlineRaw.isNotEmpty) {
524 - final int sigDeadlineInt = int.parse(sigDeadlineRaw);
525 - sigDeadlineDate = DateTime.fromMillisecondsSinceEpoch(sigDeadlineInt * 1000);
526 - }
527 - } catch (e) {
528 - // Parsing failed; we leave dates as null.
555 + } else {
556 + // Handle primitive types
557 + buffer.writeln('$fieldName: $value');
558 }
559 + }
560
531 - final permitData = {
532 - 'domainName': domainName,
533 - 'chainId': chainId,
534 - 'verifyingContract': verifyingContract,
535 - 'primaryType': primaryType,
536 - 'token': token,
537 - 'amount': amount,
538 - 'expiration': expirationDate,
539 - 'nonce': nonce,
540 - 'spender': spender,
541 - 'sigDeadline': sigDeadlineDate,
542 - };
561 + return buffer.toString();
562 + }
563
544 - return 'Domain: ${permitData['domainName']}'
545 - 'Chain ID: ${permitData['chainId']}'
546 - 'Verifying Contract: ${permitData['verifyingContract']}'
547 - 'Primary Type: ${permitData['primaryType']}'
548 - 'Token: ${permitData['token']}'
549 - 'Expiration: ${permitData['expiration'] != null ? permitData['expiration'] : 'N/A'}'
550 - 'Spender: ${permitData['spender']}'
551 - 'Signature Deadline: ${permitData['sigDeadline'] != null ? permitData['sigDeadline'] : 'N/A'}';
552 - }
553 - return '';
564 + String _toCamelCase(String input) {
565 + if (input.isEmpty) return input;
566 + return input[0].toUpperCase() + input.substring(1).toLowerCase();
567 }
568
569 Future<String> getTokenDetails(String contractAddress, String chainName) async {
lib/src/screens/wallet_connect/services/chain_service/solana/solana_chain_service.dart
+6 -20
@@ -2,7 +2,6 @@ import 'dart:convert';
2
3 import 'package:blockchain_utils/base58/base58.dart';
4 import 'package:blockchain_utils/blockchain_utils.dart' as blockchain_utils;
5 -import 'package:cake_wallet/generated/i18n.dart';
5 import 'package:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart';
6 import 'package:flutter/material.dart';
7 import 'package:on_chain/solana/solana.dart';
@@ -91,8 +90,6 @@ class SolanaChainService {
90 );
91 }
92
94 - await walletKit.respondSessionRequest(topic: topic, response: response);
95 -
93 _handleResponseForTopic(topic, response);
94 }
95
@@ -158,8 +155,6 @@ class SolanaChainService {
155 );
156 }
157
161 - await walletKit.respondSessionRequest(topic: topic, response: response);
162 -
158 _handleResponseForTopic(topic, response);
159 }
160
@@ -221,8 +216,6 @@ class SolanaChainService {
216 );
217 }
218
224 - await walletKit.respondSessionRequest(topic: topic, response: response);
225 -
219 _handleResponseForTopic(topic, response);
220 }
221
@@ -242,21 +235,14 @@ class SolanaChainService {
235 topic,
236 session!.peer.metadata.redirect,
237 response.error?.message,
238 + response.error == null,
239 );
240 } on ReownSignError catch (error) {
247 - if (error.message.contains('No matching key')) {
248 - MethodsUtils.handleRedirect(
249 - topic,
250 - session!.peer.metadata.redirect,
251 - '${S.current.error_while_processing} ${S.current.youCanGoBackToYourDapp}',
252 - );
253 - } else {
254 - MethodsUtils.handleRedirect(
255 - topic,
256 - session!.peer.metadata.redirect,
257 - error.message,
258 - );
259 - }
241 + MethodsUtils.handleRedirect(
242 + topic,
243 + session!.peer.metadata.redirect,
244 + error.message,
245 + );
246 }
247 }
248 }
lib/src/screens/wallet_connect/services/walletkit_service.dart
+1
@@ -78,6 +78,7 @@ abstract class WalletKitServiceBase with Store {
78 description: 'Cake Wallet',
79 url: 'https://cakewallet.com',
80 icons: ['https://cakewallet.com/assets/image/cake_logo.png'],
81 + redirect: Redirect(native: 'cakewallet://'),
82 ),
83 );
84
lib/src/screens/wallet_connect/widgets/wc_connection_item_widget.dart
+1 -1
@@ -85,7 +85,7 @@ class _ModelElementWidget extends StatelessWidget {
85 style: Theme.of(context).textTheme.bodyMedium!.copyWith(
86 fontWeight: FontWeight.w600,
87 ),
88 - maxLines: 10,
88 + maxLines: 50,
89 overflow: TextOverflow.ellipsis,
90 ),
91 ),
lib/src/screens/wallet_connect/widgets/wc_pairing_item_widget.dart
+25 -15
@@ -26,10 +26,18 @@ class WCPairingItemWidget extends StatelessWidget {
26 '$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
27
28 return ListTile(
29 - leading: CakeImageWidget(
30 - imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0] : null,
31 - errorWidget: CircleAvatar(
32 - backgroundImage: AssetImage('assets/images/walletconnect_logo.png'),
29 + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
30 + leading: SizedBox(
31 + width: 60,
32 + height: 60,
33 + child: CakeImageWidget(
34 + borderRadius: 8,
35 + width: 60,
36 + height: 60,
37 + imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0] : null,
38 + errorWidget: CircleAvatar(
39 + backgroundImage: AssetImage('assets/images/walletconnect_logo.png'),
40 + ),
41 ),
42 ),
43 title: Text(
@@ -59,18 +67,20 @@ class WCPairingItemWidget extends StatelessWidget {
67 ),
68 ],
69 ),
62 - trailing: Container(
63 - height: 40,
70 + trailing: SizedBox(
71 width: 44,
65 - padding: EdgeInsets.all(10),
66 - decoration: BoxDecoration(
67 - shape: BoxShape.circle,
68 - color: Theme.of(context).colorScheme.primary,
69 - ),
70 - child: Icon(
71 - Icons.edit,
72 - size: 14,
73 - color: Theme.of(context).colorScheme.onPrimary,
72 + height: 40,
73 + child: Container(
74 + padding: EdgeInsets.all(10),
75 + decoration: BoxDecoration(
76 + shape: BoxShape.circle,
77 + color: Theme.of(context).colorScheme.primary,
78 + ),
79 + child: Icon(
80 + Icons.edit,
81 + size: 14,
82 + color: Theme.of(context).colorScheme.onPrimary,
83 + ),
84 ),
85 ),
86 onTap: onTap,
lib/src/screens/wallet_connect/widgets/wc_session_auth_request_widget.dart
+5 -5
@@ -17,7 +17,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
17 child: SingleChildScrollView(child: child),
18 ),
19 const SizedBox(height: 16),
20 - Row(
20 + Column(
21 mainAxisAlignment: MainAxisAlignment.spaceEvenly,
22 children: [
23 PrimaryButton(
@@ -30,7 +30,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
30 color: Theme.of(context).colorScheme.error,
31 textColor: Theme.of(context).colorScheme.onError,
32 ),
33 - const SizedBox(width: 8),
33 + const SizedBox(height: 8),
34 PrimaryButton(
35 onPressed: () {
36 if (Navigator.canPop(context)) {
@@ -41,7 +41,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
41 color: Theme.of(context).colorScheme.primary,
42 textColor: Theme.of(context).colorScheme.onPrimary,
43 ),
44 - const SizedBox(width: 8),
44 + const SizedBox(height: 8),
45 PrimaryButton(
46 onPressed: () {
47 if (Navigator.canPop(context)) {
@@ -49,8 +49,8 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
49 }
50 },
51 text: S.current.sign_all,
52 - color: Theme.of(context).secondaryHeaderColor,
53 - textColor: Theme.of(context).colorScheme.onPrimary,
52 + color: Theme.of(context).colorScheme.secondaryContainer,
53 + textColor: Theme.of(context).colorScheme.onSecondaryContainer,
54 ),
55 ],
56 ),
lib/src/widgets/cake_image_widget.dart
+3 -1
@@ -11,6 +11,7 @@ class CakeImageWidget extends StatelessWidget {
11 this.loadingWidget,
12 this.errorWidget,
13 this.color,
14 + this.borderRadius = 24.0,
15 });
16
17 final String? imageUrl;
@@ -20,6 +21,7 @@ class CakeImageWidget extends StatelessWidget {
21 final Widget? loadingWidget;
22 final Widget? errorWidget;
23 final Color? color;
24 + final double borderRadius;
25
26 @override
27 Widget build(BuildContext context) {
@@ -80,7 +82,7 @@ class CakeImageWidget extends StatelessWidget {
82 height: height,
83 width: width,
84 decoration: BoxDecoration(
83 - borderRadius: BorderRadius.circular(24.0),
85 + borderRadius: BorderRadius.circular(borderRadius),
86 color: Theme.of(context).colorScheme.surfaceContainerHighest,
87 ),
88 child: Center(
lib/src/widgets/provider_optoin_tile.dart
+13
@@ -289,6 +289,15 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo
289 child: CircularProgressIndicator(),
290 ),
291 ),
292 + errorBuilder: (_, __, ___) {
293 + return Container(
294 + height: imageHeight,
295 + width: imageWidth,
296 + child: Center(
297 + child: Icon(Icons.error_outline, color: Colors.grey),
298 + ),
299 + );
300 + },
301 )
302 : Image.network(
303 imagePath,
@@ -315,6 +324,9 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo
324 return Container(
325 height: imageHeight,
326 width: imageWidth,
327 + child: Center(
328 + child: Icon(Icons.error_outline, color: Colors.grey),
329 + ),
330 );
331 },
332 );
@@ -325,6 +337,7 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo
337 height: imageHeight,
338 width: imageWidth,
339 colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
340 + errorBuilder: (_, __, ___) => Icon(Icons.error, color: Colors.grey),
341 )
342 : Image.asset(imagePath, height: imageHeight, width: imageWidth);
343 }
lib/utils/exception_handler.dart
+6
@@ -274,6 +274,12 @@ class ExceptionHandler {
274 "NetworkImage._loadAsync",
275 "SSLV3_ALERT_BAD_RECORD_MAC",
276 "PlatformException(already_active, File picker is already active",
277 + // SVG-related errors
278 + "SvgParser",
279 + "SVG parsing error",
280 + "Invalid SVG",
281 + "SVG format error",
282 + "SvgPicture",
283 // Temporary ignored, More context: Flutter secure storage reads the values as null some times
284 // probably when the device was locked and then opened on Cake
285 // this is solved by a restart of the app
lib/utils/image_utill.dart
+13
@@ -26,6 +26,15 @@ class ImageUtil {
26 child: CircularProgressIndicator(),
27 ),
28 ),
29 + errorBuilder: (_, __, ___) {
30 + return Container(
31 + height: _height,
32 + width: _width,
33 + child: Center(
34 + child: Icon(Icons.error_outline, color: Colors.grey),
35 + ),
36 + );
37 + },
38 )
39 : Image.network(
40 key: ValueKey(imagePath),
@@ -54,6 +63,9 @@ class ImageUtil {
63 return Container(
64 height: _height,
65 width: _width,
66 + child: Center(
67 + child: Icon(Icons.error_outline, color: Colors.grey),
68 + ),
69 );
70 },
71 );
@@ -64,6 +76,7 @@ class ImageUtil {
76 height: _height,
77 width: _width,
78 placeholderBuilder: (_) => Icon(Icons.error),
79 + errorBuilder: (_, __, ___) => Icon(Icons.error),
80 key: ValueKey(imagePath),
81 )
82 : Image.asset(