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,
|
topic: topic,
|
||||||
response: response,
|
response: response,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
MethodsUtils.handleRedirect(
|
MethodsUtils.handleRedirect(
|
||||||
topic,
|
topic,
|
||||||
session!.peer.metadata.redirect,
|
session.peer.metadata.redirect,
|
||||||
response.error?.message,
|
response.error?.message,
|
||||||
response.error == null,
|
response.error == null,
|
||||||
);
|
);
|
||||||
} on ReownSignError catch (error) {
|
} on ReownSignError catch (error) {
|
||||||
|
if (session == null) return;
|
||||||
|
|
||||||
MethodsUtils.handleRedirect(
|
MethodsUtils.handleRedirect(
|
||||||
topic,
|
topic,
|
||||||
session!.peer.metadata.redirect,
|
session.peer.metadata.redirect,
|
||||||
error.message,
|
error.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -489,68 +494,76 @@ class EvmChainServiceImpl {
|
||||||
final typedData = jsonDecode(data[1] as String) as Map<String, dynamic>;
|
final typedData = jsonDecode(data[1] as String) as Map<String, dynamic>;
|
||||||
|
|
||||||
// Extracting domain details.
|
// 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 domainName = domain['name']?.toString() ?? '';
|
||||||
|
final version = domain['version']?.toString() ?? '';
|
||||||
|
final chainId = domain['chainId']?.toString() ?? '';
|
||||||
final verifyingContract = domain['verifyingContract']?.toString() ?? '';
|
final verifyingContract = domain['verifyingContract']?.toString() ?? '';
|
||||||
|
|
||||||
final chainId = domain['chainId']?.toString() ?? '';
|
// Get the primary type and types
|
||||||
final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type);
|
|
||||||
|
|
||||||
// Get the primary type.
|
|
||||||
final primaryType = typedData['primaryType']?.toString() ?? '';
|
final primaryType = typedData['primaryType']?.toString() ?? '';
|
||||||
|
final types = typedData['types'] as Map<String, dynamic>? ?? {};
|
||||||
|
final message = typedData['message'] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
// Extracting message details.
|
// Build a readable message based on the primary type and its structure
|
||||||
final message = typedData['message'] ?? {} as Map<String, dynamic>;
|
String messageDetails = '';
|
||||||
final details = message['details'] ?? {} as Map<String, dynamic>;
|
|
||||||
final amount = details['amount']?.toString() ?? '';
|
|
||||||
final expirationRaw = details['expiration']?.toString() ?? '';
|
|
||||||
final nonce = details['nonce']?.toString() ?? '';
|
|
||||||
|
|
||||||
final tokenAddress = details['token']?.toString() ?? '';
|
if (types.containsKey(primaryType)) {
|
||||||
final token = await getTokenDetails(tokenAddress, chainName);
|
final typeFields = types[primaryType] as List<dynamic>;
|
||||||
|
messageDetails = _formatMessageFields(message, typeFields, types);
|
||||||
final spender = message['spender']?.toString() ?? '';
|
} else {
|
||||||
final sigDeadlineRaw = message['sigDeadline']?.toString() ?? '';
|
// For unknown types, show the raw message
|
||||||
|
messageDetails = message.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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final permitData = {
|
return '''Domain Name: $domainName
|
||||||
'domainName': domainName,
|
Version: $version
|
||||||
'chainId': chainId,
|
Chain ID: $chainId
|
||||||
'verifyingContract': verifyingContract,
|
Verifying Contract: $verifyingContract
|
||||||
'primaryType': primaryType,
|
Primary Type: $primaryType\n
|
||||||
'token': token,
|
Message:
|
||||||
'amount': amount,
|
$messageDetails''';
|
||||||
'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 '';
|
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 {
|
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/base58/base58.dart';
|
||||||
import 'package:blockchain_utils/blockchain_utils.dart' as blockchain_utils;
|
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:cake_wallet/src/screens/wallet_connect/services/chain_service/solana/solana_supported_methods.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:on_chain/solana/solana.dart';
|
import 'package:on_chain/solana/solana.dart';
|
||||||
|
@ -91,8 +90,6 @@ class SolanaChainService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await walletKit.respondSessionRequest(topic: topic, response: response);
|
|
||||||
|
|
||||||
_handleResponseForTopic(topic, response);
|
_handleResponseForTopic(topic, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,8 +155,6 @@ class SolanaChainService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await walletKit.respondSessionRequest(topic: topic, response: response);
|
|
||||||
|
|
||||||
_handleResponseForTopic(topic, response);
|
_handleResponseForTopic(topic, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,8 +216,6 @@ class SolanaChainService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await walletKit.respondSessionRequest(topic: topic, response: response);
|
|
||||||
|
|
||||||
_handleResponseForTopic(topic, response);
|
_handleResponseForTopic(topic, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,21 +235,14 @@ class SolanaChainService {
|
||||||
topic,
|
topic,
|
||||||
session!.peer.metadata.redirect,
|
session!.peer.metadata.redirect,
|
||||||
response.error?.message,
|
response.error?.message,
|
||||||
|
response.error == null,
|
||||||
);
|
);
|
||||||
} on ReownSignError catch (error) {
|
} on ReownSignError catch (error) {
|
||||||
if (error.message.contains('No matching key')) {
|
MethodsUtils.handleRedirect(
|
||||||
MethodsUtils.handleRedirect(
|
topic,
|
||||||
topic,
|
session!.peer.metadata.redirect,
|
||||||
session!.peer.metadata.redirect,
|
error.message,
|
||||||
'${S.current.error_while_processing} ${S.current.youCanGoBackToYourDapp}',
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
MethodsUtils.handleRedirect(
|
|
||||||
topic,
|
|
||||||
session!.peer.metadata.redirect,
|
|
||||||
error.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ abstract class WalletKitServiceBase with Store {
|
||||||
description: 'Cake Wallet',
|
description: 'Cake Wallet',
|
||||||
url: 'https://cakewallet.com',
|
url: 'https://cakewallet.com',
|
||||||
icons: ['https://cakewallet.com/assets/image/cake_logo.png'],
|
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(
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
maxLines: 10,
|
maxLines: 50,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -26,10 +26,18 @@ class WCPairingItemWidget extends StatelessWidget {
|
||||||
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
|
'$year-${month.toString().padLeft(2, '0')}-${day.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: CakeImageWidget(
|
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
imageUrl: metadata.icons.isNotEmpty ? metadata.icons[0] : null,
|
leading: SizedBox(
|
||||||
errorWidget: CircleAvatar(
|
width: 60,
|
||||||
backgroundImage: AssetImage('assets/images/walletconnect_logo.png'),
|
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(
|
title: Text(
|
||||||
|
@ -59,18 +67,20 @@ class WCPairingItemWidget extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: Container(
|
trailing: SizedBox(
|
||||||
height: 40,
|
|
||||||
width: 44,
|
width: 44,
|
||||||
padding: EdgeInsets.all(10),
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
child: Container(
|
||||||
shape: BoxShape.circle,
|
padding: EdgeInsets.all(10),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
decoration: BoxDecoration(
|
||||||
),
|
shape: BoxShape.circle,
|
||||||
child: Icon(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
Icons.edit,
|
),
|
||||||
size: 14,
|
child: Icon(
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
Icons.edit,
|
||||||
|
size: 14,
|
||||||
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
|
|
@ -17,7 +17,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
|
||||||
child: SingleChildScrollView(child: child),
|
child: SingleChildScrollView(child: child),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
|
@ -30,7 +30,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
|
||||||
color: Theme.of(context).colorScheme.error,
|
color: Theme.of(context).colorScheme.error,
|
||||||
textColor: Theme.of(context).colorScheme.onError,
|
textColor: Theme.of(context).colorScheme.onError,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 8),
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (Navigator.canPop(context)) {
|
if (Navigator.canPop(context)) {
|
||||||
|
@ -41,7 +41,7 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(height: 8),
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (Navigator.canPop(context)) {
|
if (Navigator.canPop(context)) {
|
||||||
|
@ -49,8 +49,8 @@ class WCSessionAuthRequestWidget extends StatelessWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text: S.current.sign_all,
|
text: S.current.sign_all,
|
||||||
color: Theme.of(context).secondaryHeaderColor,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -11,6 +11,7 @@ class CakeImageWidget extends StatelessWidget {
|
||||||
this.loadingWidget,
|
this.loadingWidget,
|
||||||
this.errorWidget,
|
this.errorWidget,
|
||||||
this.color,
|
this.color,
|
||||||
|
this.borderRadius = 24.0,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? imageUrl;
|
final String? imageUrl;
|
||||||
|
@ -20,6 +21,7 @@ class CakeImageWidget extends StatelessWidget {
|
||||||
final Widget? loadingWidget;
|
final Widget? loadingWidget;
|
||||||
final Widget? errorWidget;
|
final Widget? errorWidget;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -80,7 +82,7 @@ class CakeImageWidget extends StatelessWidget {
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(24.0),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|
|
@ -289,6 +289,15 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
errorBuilder: (_, __, ___) {
|
||||||
|
return Container(
|
||||||
|
height: imageHeight,
|
||||||
|
width: imageWidth,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(Icons.error_outline, color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
)
|
)
|
||||||
: Image.network(
|
: Image.network(
|
||||||
imagePath,
|
imagePath,
|
||||||
|
@ -315,6 +324,9 @@ Widget getImage(String imagePath, {double? height, double? width, Color? imageCo
|
||||||
return Container(
|
return Container(
|
||||||
height: imageHeight,
|
height: imageHeight,
|
||||||
width: imageWidth,
|
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,
|
height: imageHeight,
|
||||||
width: imageWidth,
|
width: imageWidth,
|
||||||
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
|
colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null,
|
||||||
|
errorBuilder: (_, __, ___) => Icon(Icons.error, color: Colors.grey),
|
||||||
)
|
)
|
||||||
: Image.asset(imagePath, height: imageHeight, width: imageWidth);
|
: Image.asset(imagePath, height: imageHeight, width: imageWidth);
|
||||||
}
|
}
|
||||||
|
|
|
@ -274,6 +274,12 @@ class ExceptionHandler {
|
||||||
"NetworkImage._loadAsync",
|
"NetworkImage._loadAsync",
|
||||||
"SSLV3_ALERT_BAD_RECORD_MAC",
|
"SSLV3_ALERT_BAD_RECORD_MAC",
|
||||||
"PlatformException(already_active, File picker is already active",
|
"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
|
// 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
|
// probably when the device was locked and then opened on Cake
|
||||||
// this is solved by a restart of the app
|
// this is solved by a restart of the app
|
||||||
|
|
|
@ -26,6 +26,15 @@ class ImageUtil {
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
errorBuilder: (_, __, ___) {
|
||||||
|
return Container(
|
||||||
|
height: _height,
|
||||||
|
width: _width,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(Icons.error_outline, color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
)
|
)
|
||||||
: Image.network(
|
: Image.network(
|
||||||
key: ValueKey(imagePath),
|
key: ValueKey(imagePath),
|
||||||
|
@ -54,6 +63,9 @@ class ImageUtil {
|
||||||
return Container(
|
return Container(
|
||||||
height: _height,
|
height: _height,
|
||||||
width: _width,
|
width: _width,
|
||||||
|
child: Center(
|
||||||
|
child: Icon(Icons.error_outline, color: Colors.grey),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -64,6 +76,7 @@ class ImageUtil {
|
||||||
height: _height,
|
height: _height,
|
||||||
width: _width,
|
width: _width,
|
||||||
placeholderBuilder: (_) => Icon(Icons.error),
|
placeholderBuilder: (_) => Icon(Icons.error),
|
||||||
|
errorBuilder: (_, __, ___) => Icon(Icons.error),
|
||||||
key: ValueKey(imagePath),
|
key: ValueKey(imagePath),
|
||||||
)
|
)
|
||||||
: Image.asset(
|
: Image.asset(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue