mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
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:
parent
af89603b81
commit
65bb917bfb
10 changed files with 141 additions and 97 deletions
|
@ -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 {
|
||||
|
|
|
@ -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,21 +235,14 @@ 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,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
MethodsUtils.handleRedirect(
|
||||
topic,
|
||||
session!.peer.metadata.redirect,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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://'),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -26,10 +26,18 @@ class WCPairingItemWidget extends StatelessWidget {
|
|||
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
|
||||
|
||||
return ListTile(
|
||||
leading: CakeImageWidget(
|
||||
imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0] : null,
|
||||
errorWidget: CircleAvatar(
|
||||
backgroundImage: AssetImage('assets/images/walletconnect_logo.png'),
|
||||
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(
|
||||
|
@ -59,18 +67,20 @@ class WCPairingItemWidget extends StatelessWidget {
|
|||
),
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
height: 40,
|
||||
trailing: SizedBox(
|
||||
width: 44,
|
||||
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,
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue