CW-1190: Minor Fixes (#2614)
What changed, and why it matters
This commit is a routine bug-fix patch titled 'Minor Fixes'. It mainly does two things: it prevents users from accidentally sending cryptocurrency while their wallet is still synchronizing, and it makes WalletConnect URI handling more robust by trimming whitespace, stripping stray '@' prefixes, and handling session expiry errors more gracefully. There is no direct evidence in the commit that these fixes address an active security exploit; they appear to be defensive hardening and usability improvements.
Treat as a defensive hardening patch. Review whether `isReadyForSend` correctly reflects wallet synchronization state across all supported chains, and verify that the new WalletConnect URI sanitization does not inadvertently strip valid URI variants. No urgent security response is indicated by the commit alone.
Security signals we found
UI control now blocks transaction confirmation while wallet is not synchronized
WalletConnect URI parsing hardened against malformed/decorated input
WalletConnect session expiry error now triggers pairing cleanup
Use of `Uri.tryParse` and scheme validation replaces direct `Uri.parse` calls
Evidence from the diff
The patch hardens send-flow UI controls by plumbing a new isSlideActionEnabled flag through BaseBottomSheet, ConfirmSendingBottomSheet, and StandardSlideButton, disabling the swipe-to-send gesture and accessible-navigation button when sendViewModel.isReadyForSend is false. This guards against accidental transaction submission while the wallet is not fully synced. It also improves WalletConnect URI parsing in wc_connections_listing_view.dart by trimming input, decoding uri= query parameters, stripping leading ‘@’ characters, and using Uri.tryParse with scheme validation. Additionally, walletkit_service.dart now catches ReownSignError code 6 (session expired) and deletes the stale pairing. The changes are defensive and reduce attack surface, but the commit does not disclose a specific vulnerability or credit an external reporter.
Changed components
lib/src/screens/send/send_page.dartlib/src/screens/exchange_trade/exchange_trade_page.dartlib/src/screens/transaction_details/rbf_details_page.dartlib/cake_pay/src/cards/cake_pay_buy_card_page.dartlib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dartlib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dartlib/src/widgets/standard_slide_button_widget.dartlib/src/screens/wallet_connect/wc_connections_listing_view.dartlib/src/screens/wallet_connect/services/walletkit_service.dartInspect captured patch +79 / −30
diff --git a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
index 1d24a5ad..bf6dc1fd 100644
--- a/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
+++ b/lib/cake_pay/src/cards/cake_pay_buy_card_page.dart
@@ -627,6 +627,7 @@ class CakePayBuyCardPage extends BasePage {
feeFiatAmount: _sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: displayingOutputs,
footerType: FooterType.slideActionButton,
+ isSlideActionEnabled: _sendViewModel.isReadyForSend,
slideActionButtonText:
cakePayBuyCardViewModel.isSimulating ? 'Swipe to simulate' : 'Swipe to send',
accessibleNavigationModeSlideActionButtonText:
diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart
index 67f05c74..7a1587d8 100644
--- a/lib/src/screens/exchange_trade/exchange_trade_page.dart
+++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart
@@ -284,6 +284,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
return ConfirmSendingBottomSheet(
key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'),
footerType: FooterType.slideActionButton,
+ isSlideActionEnabled: widget.exchangeTradeViewModel.sendViewModel.isReadyForSend,
walletType: widget.exchangeTradeViewModel.sendViewModel.walletType,
titleText: S.of(bottomSheetContext).confirm_transaction,
titleIconPath:
diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart
index 3ffd3158..f5b15148 100644
--- a/lib/src/screens/send/send_page.dart
+++ b/lib/src/screens/send/send_page.dart
@@ -567,6 +567,7 @@ class SendPage extends BasePage {
titleText: S.of(bottomSheetContext).confirm_transaction,
accessibleNavigationModeSlideActionButtonText: S.of(bottomSheetContext).send,
footerType: FooterType.slideActionButton,
+ isSlideActionEnabled: sendViewModel.isReadyForSend,
walletType: sendViewModel.walletType,
titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath,
currency: sendViewModel.selectedCryptoCurrency,
diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart
index 521cf063..66866bf2 100644
--- a/lib/src/screens/transaction_details/rbf_details_page.dart
+++ b/lib/src/screens/transaction_details/rbf_details_page.dart
@@ -190,6 +190,7 @@ class RBFDetailsPage extends BasePage {
return ConfirmSendingBottomSheet(
key: ValueKey('rbf_confirm_sending_bottom_sheet'),
titleText: S.of(bottomSheetContext).confirm_transaction,
+ isSlideActionEnabled: transactionDetailsViewModel.sendViewModel.isReadyForSend,
walletType: transactionDetailsViewModel.sendViewModel.walletType,
titleIconPath:
transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath,
diff --git a/lib/src/screens/wallet_connect/services/walletkit_service.dart b/lib/src/screens/wallet_connect/services/walletkit_service.dart
index 4c25032a..9cdbe8eb 100644
--- a/lib/src/screens/wallet_connect/services/walletkit_service.dart
+++ b/lib/src/screens/wallet_connect/services/walletkit_service.dart
@@ -202,6 +202,13 @@ abstract class WalletKitServiceBase with Store {
),
);
}
+ } on ReownSignError catch (e) {
+ if (e.code == 6) {
+ try {
+ await deletePairing(topic: session.pairingTopic);
+ } catch (_) {}
+ _refreshPairings();
+ }
} catch (_) {}
}
}
diff --git a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart
index 9fcdb61f..46618a6e 100644
--- a/lib/src/screens/wallet_connect/wc_connections_listing_view.dart
+++ b/lib/src/screens/wallet_connect/wc_connections_listing_view.dart
@@ -33,9 +33,13 @@ class WalletConnectConnectionsView extends StatelessWidget {
final query = actualLinkList[1];
- final uri = Uri.decodeComponent(query);
+ final decoded = Uri.decodeComponent(query).trim();
- final uriData = Uri.parse(uri);
+ final sanitized = decoded.startsWith('@') ? decoded.substring(1) : decoded;
+
+ final uriData = Uri.tryParse(sanitized);
+
+ if (uriData == null || (uriData.scheme.isEmpty)) return;
await walletKitService.pairWithUri(uriData);
}
@@ -88,7 +92,25 @@ class WCPairingsWidget extends BasePage {
if (walletConnectURI == null) return _invalidUriToast(context, S.current.nullURIError);
log('_onFoundUri: $walletConnectURI');
- final Uri uriData = Uri.parse(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);
+ }
+ }
+
+ // 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) {
+ return _invalidUriToast(context, S.current.invalid_input);
+ }
await walletKitService.pairWithUri(uriData);
}
@@ -121,10 +143,10 @@ class WCPairingsWidget extends BasePage {
Text(
S.current.connectWalletPrompt,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
- fontSize: 16.0,
- fontWeight: FontWeight.normal,
- color: Theme.of(context).colorScheme.onSurface,
- ),
+ fontSize: 16.0,
+ fontWeight: FontWeight.normal,
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
),
SizedBox(height: 16),
PrimaryButton(
@@ -156,10 +178,10 @@ class WCPairingsWidget extends BasePage {
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,
- ),
+ fontSize: 16.0,
+ fontWeight: FontWeight.normal,
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
),
),
replacement: ListView.builder(
diff --git a/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart
index 62fcdd22..37497282 100644
--- a/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart
+++ b/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart
@@ -12,6 +12,7 @@ abstract class BaseBottomSheet extends StatelessWidget {
required this.footerType,
this.slideActionButtonText,
this.onSlideActionComplete,
+ this.isSlideActionEnabled = true,
this.singleActionButtonText,
this.accessibleNavigationModeSlideActionButtonText,
this.onSingleActionButtonPressed,
@@ -30,6 +31,7 @@ abstract class BaseBottomSheet extends StatelessWidget {
final FooterType footerType;
final String? slideActionButtonText;
final VoidCallback? onSlideActionComplete;
+ final bool isSlideActionEnabled;
final String? singleActionButtonText;
final String? accessibleNavigationModeSlideActionButtonText;
final VoidCallback? onSingleActionButtonPressed;
@@ -112,6 +114,7 @@ abstract class BaseBottomSheet extends StatelessWidget {
buttonText: slideActionButtonText ?? '',
onSlideComplete: onSlideActionComplete ?? () {},
accessibleNavigationModeButtonText: accessibleNavigationModeSlideActionButtonText ?? '',
+ isDisabled: !isSlideActionEnabled,
),
);
diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart
index 4bac63a2..1f9b09d5 100644
--- a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart
+++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart
@@ -21,6 +21,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
String? titleIconPath,
String? slideActionButtonText,
VoidCallback? onSlideActionComplete,
+ bool isSlideActionEnabled = true,
String? accessibleNavigationModeSlideActionButtonText,
required this.currency,
this.paymentId,
@@ -48,6 +49,7 @@ class ConfirmSendingBottomSheet extends BaseBottomSheet {
footerType: footerType,
slideActionButtonText: slideActionButtonText ?? 'Swipe to send',
onSlideActionComplete: onSlideActionComplete,
+ isSlideActionEnabled: isSlideActionEnabled,
accessibleNavigationModeSlideActionButtonText:
accessibleNavigationModeSlideActionButtonText,
key: key);
diff --git a/lib/src/widgets/standard_slide_button_widget.dart b/lib/src/widgets/standard_slide_button_widget.dart
index d531cfb6..3f6b9ad5 100644
--- a/lib/src/widgets/standard_slide_button_widget.dart
+++ b/lib/src/widgets/standard_slide_button_widget.dart
@@ -11,6 +11,7 @@ class StandardSlideButton extends StatefulWidget {
required this.accessibleNavigationModeButtonText,
this.tileBackgroundColor,
this.knobColor,
+ this.isDisabled = false,
}) : super(key: key);
final VoidCallback onSlideComplete;
@@ -19,6 +20,7 @@ class StandardSlideButton extends StatefulWidget {
final String accessibleNavigationModeButtonText;
final Color? tileBackgroundColor;
final Color? knobColor;
+ final bool isDisabled;
@override
StandardSlideButtonState createState() => StandardSlideButtonState();
@@ -36,14 +38,16 @@ class StandardSlideButtonState extends State<StandardSlideButton> {
Widget build(BuildContext context) {
final bool accessible = MediaQuery.of(context).accessibleNavigation;
- final tileBackgroundColor = context.currentTheme.customColors.backgroundGradientColor;
+ final tileBackgroundColor = widget.isDisabled
+ ? context.currentTheme.customColors.backgroundGradientColor.withOpacity(0.5)
+ : context.currentTheme.customColors.backgroundGradientColor;
return accessible
? PrimaryButton(
text: widget.accessibleNavigationModeButtonText,
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
- onPressed: () => widget.onSlideComplete(),
+ onPressed: widget.isDisabled ? null : () => widget.onSlideComplete(),
)
: LayoutBuilder(
builder: (context, constraints) {
@@ -56,7 +60,10 @@ class StandardSlideButtonState extends State<StandardSlideButton> {
height: widget.height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
- color: widget.tileBackgroundColor ?? tileBackgroundColor,
+ color: (widget.isDisabled
+ ? widget.tileBackgroundColor?.withOpacity(0.5)
+ : widget.tileBackgroundColor) ??
+ tileBackgroundColor,
),
child: Stack(
alignment: Alignment.centerLeft,
@@ -74,22 +81,26 @@ class StandardSlideButtonState extends State<StandardSlideButton> {
left: sideMargin + _dragPosition,
child: GestureDetector(
key: ValueKey('standard_slide_button_widget_slider_key'),
- onHorizontalDragUpdate: (details) {
- setState(() {
- _dragPosition += details.delta.dx;
- if (_dragPosition < 0) _dragPosition = 0;
- if (_dragPosition > effectiveMaxWidth - sliderWidth) {
- _dragPosition = effectiveMaxWidth - sliderWidth;
- }
- });
- },
- onHorizontalDragEnd: (details) {
- if (_dragPosition >= effectiveMaxWidth - sliderWidth - 10) {
- widget.onSlideComplete();
- } else {
- setState(() => _dragPosition = 0);
- }
- },
+ onHorizontalDragUpdate: widget.isDisabled
+ ? null
+ : (details) {
+ setState(() {
+ _dragPosition += details.delta.dx;
+ if (_dragPosition < 0) _dragPosition = 0;
+ if (_dragPosition > effectiveMaxWidth - sliderWidth) {
+ _dragPosition = effectiveMaxWidth - sliderWidth;
+ }
+ });
+ },
+ onHorizontalDragEnd: widget.isDisabled
+ ? null
+ : (details) {
+ if (_dragPosition >= effectiveMaxWidth - sliderWidth - 10) {
+ widget.onSlideComplete();
+ } else {
+ setState(() => _dragPosition = 0);
+ }
+ },
child: Container(
key: ValueKey('standard_slide_button_widget_slider_container_key'),
width: sliderWidth,
Why this scored 48/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.