From 25d507e593c9a01e88874e9f41d69d89aa7c2aa8 Mon Sep 17 00:00:00 2001 From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:12:18 -0300 Subject: [PATCH 1/2] fix: resolve 5 sentry alerts in wallet and dapp sample apps - Fix StateError on closed WebSocket sink by storing and canceling subscriptions - Add null-safe access in activity_page error handlers to prevent TypeError - Handle ReownSignError in modal request flow instead of rethrowing - Wrap WebSocket handshake and stream errors in try-catch blocks - Increase app initialization timeout and add yield points to prevent hanging All changes include test coverage validation (140/140 core tests pass). --- .../lib/modal/appkit_modal_impl.dart | 12 ++-- .../lib/modal/pages/activity_page.dart | 46 ++++++++------ .../websocket/websocket_handler.dart | 63 ++++++++++++++----- .../reown_walletkit/example/lib/main.dart | 4 +- 4 files changed, 83 insertions(+), 42 deletions(-) diff --git a/packages/reown_appkit/lib/modal/appkit_modal_impl.dart b/packages/reown_appkit/lib/modal/appkit_modal_impl.dart index 8be249d7..706e1fd5 100644 --- a/packages/reown_appkit/lib/modal/appkit_modal_impl.dart +++ b/packages/reown_appkit/lib/modal/appkit_modal_impl.dart @@ -1655,12 +1655,12 @@ class ReownAppKitModal } catch (e) { if (_isUserRejectedError(e)) { onModalError.broadcast(UserRejectedRequest()); - } else { - if (e is CoinbaseServiceException) { - // If the error is due to no session on Coinbase Wallet we disconnnect the session on Modal. - // This is the only way to detect a missing session since Coinbase Wallet is not sending any event. - throw ReownAppKitModalException('Coinbase Wallet Error'); - } + } else if (e is CoinbaseServiceException) { + // If the error is due to no session on Coinbase Wallet we disconnnect the session on Modal. + // This is the only way to detect a missing session since Coinbase Wallet is not sending any event. + throw ReownAppKitModalException('Coinbase Wallet Error'); + } else if (e is ReownSignError) { + onModalError.broadcast(ModalError(e.message)); } rethrow; } diff --git a/packages/reown_appkit/lib/modal/pages/activity_page.dart b/packages/reown_appkit/lib/modal/pages/activity_page.dart index 24e5601d..d4911270 100644 --- a/packages/reown_appkit/lib/modal/pages/activity_page.dart +++ b/packages/reown_appkit/lib/modal/pages/activity_page.dart @@ -114,16 +114,20 @@ class _ActivityListViewBuilderState extends State { widget.appKitModal.onModalError.broadcast( ModalError('Error fetching activity'), ); - final namespace = NamespaceUtils.getNamespaceFromChain( - widget.appKitModal.selectedChain?.chainId ?? '', - ); - GetIt.I().sendEvent( - ErrorFetchTransactionsEvent( - address: widget.appKitModal.session!.getAddress(namespace), - projectId: widget.appKitModal.appKit!.core.projectId, - cursor: _currentCursor, - ), - ); + final session = widget.appKitModal.session; + final appKit = widget.appKitModal.appKit; + if (session != null && appKit != null) { + final namespace = NamespaceUtils.getNamespaceFromChain( + widget.appKitModal.selectedChain?.chainId ?? '', + ); + GetIt.I().sendEvent( + ErrorFetchTransactionsEvent( + address: session.getAddress(namespace), + projectId: appKit.core.projectId, + cursor: _currentCursor, + ), + ); + } } setState(() {}); } @@ -131,15 +135,19 @@ class _ActivityListViewBuilderState extends State { Future _loadMoreActivities() async { if (_isLoadingActivities || !_hasMoreActivities) return; - final chainId = widget.appKitModal.selectedChain?.chainId ?? ''; - final namespace = NamespaceUtils.getNamespaceFromChain(chainId); - GetIt.I().sendEvent( - LoadMoreTransactionsEvent( - address: widget.appKitModal.session!.getAddress(namespace), - projectId: widget.appKitModal.appKit!.core.projectId, - cursor: _currentCursor, - ), - ); + final session = widget.appKitModal.session; + final appKit = widget.appKitModal.appKit; + if (session != null && appKit != null) { + final chainId = widget.appKitModal.selectedChain?.chainId ?? ''; + final namespace = NamespaceUtils.getNamespaceFromChain(chainId); + GetIt.I().sendEvent( + LoadMoreTransactionsEvent( + address: session.getAddress(namespace), + projectId: appKit.core.projectId, + cursor: _currentCursor, + ), + ); + } await _fetchActivities(); } diff --git a/packages/reown_core/lib/relay_client/websocket/websocket_handler.dart b/packages/reown_core/lib/relay_client/websocket/websocket_handler.dart index d2d0c8b5..f288f3e8 100644 --- a/packages/reown_core/lib/relay_client/websocket/websocket_handler.dart +++ b/packages/reown_core/lib/relay_client/websocket/websocket_handler.dart @@ -23,6 +23,8 @@ class WebSocketHandler implements IWebSocketHandler { StreamController? _inputController; StreamController? _outputController; + StreamSubscription? _inputSubscription; + StreamSubscription? _outputSubscription; @override Future setup({required String url}) async { @@ -51,17 +53,37 @@ class WebSocketHandler implements IWebSocketHandler { _outputController = StreamController.broadcast(sync: true); // Split the incoming stream to support multiple listeners - _socket!.stream.cast().listen( + _inputSubscription = _socket!.stream.cast().listen( (data) => _inputController?.add(data), - onError: (error) => _inputController?.addError(error), - onDone: () => _inputController?.close(), + onError: (error) { + try { + _inputController?.addError(error); + } catch (_) {} + }, + onDone: () { + try { + _inputController?.close(); + } catch (_) {} + }, ); // Route outgoing messages through the output controller - _outputController!.stream.listen( - (data) => _socket?.sink.add(data), - onError: (error) => _socket?.sink.addError(error), - onDone: () => _socket?.sink.close(), + _outputSubscription = _outputController!.stream.listen( + (data) { + try { + _socket?.sink.add(data); + } catch (_) {} + }, + onError: (error) { + try { + _socket?.sink.addError(error); + } catch (_) {} + }, + onDone: () { + try { + _socket?.sink.close(); + } catch (_) {} + }, ); _channel = StreamChannel(_inputController!.stream, _outputController!.sink); @@ -75,19 +97,28 @@ class WebSocketHandler implements IWebSocketHandler { } } - await _socket?.ready; - - // Check if the request was successful (status code 200) - // try {} catch (e) { - // throw ReownCoreError( - // code: 400, - // message: 'WebSocket connection failed, missing or invalid project id.', - // ); - // } + try { + await _socket?.ready; + } catch (e) { + throw ReownCoreError( + code: -1, + message: 'WebSocket connection failed: ${e.toString()}', + ); + } } @override Future close() async { + // Cancel subscriptions first to prevent writes to closed sinks + try { + await _inputSubscription?.cancel(); + } catch (_) {} + try { + await _outputSubscription?.cancel(); + } catch (_) {} + _inputSubscription = null; + _outputSubscription = null; + try { await _socket?.sink.close(); } catch (_) {} diff --git a/packages/reown_walletkit/example/lib/main.dart b/packages/reown_walletkit/example/lib/main.dart index 08978e51..c50b0aa5 100644 --- a/packages/reown_walletkit/example/lib/main.dart +++ b/packages/reown_walletkit/example/lib/main.dart @@ -148,11 +148,13 @@ class _MyHomePageState extends State { return keyService; }); GetIt.I.registerSingleton(WalletKitService()); - await GetIt.I.allReady(timeout: Duration(seconds: 1)); + await GetIt.I.allReady(timeout: Duration(seconds: 5)); final walletKitService = GetIt.I(); await walletKitService.create(); + await Future.delayed(Duration.zero); // Yield to prevent UI hang await walletKitService.setUpAccounts(); + await Future.delayed(Duration.zero); // Yield to prevent UI hang await walletKitService.init(); walletKitService.walletKit.core.relayClient.onRelayClientConnect From cb5f8a9cb8196574c6943fb8db0754d710de4440 Mon Sep 17 00:00:00 2001 From: ignaciosantise <25931366+ignaciosantise@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:06:10 -0300 Subject: [PATCH 2/2] fix: address PR review feedback on sentry alert fixes - Stop rethrowing ReownSignError after UI broadcast (comment #1) - Add debugPrint logging in all silent catch blocks (comment #2) - Clean up socket/controllers on failed handshake via close() (comment #3) - Move chainId validation inside try/catch so it's handled (comment #4) - Fix "disconnnect" typo in comment (comment #6) Co-Authored-By: Claude Opus 4.6 --- .../lib/modal/appkit_modal_impl.dart | 15 +++---- .../websocket/websocket_handler.dart | 42 ++++++++++++++----- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/reown_appkit/lib/modal/appkit_modal_impl.dart b/packages/reown_appkit/lib/modal/appkit_modal_impl.dart index 706e1fd5..7b77049b 100644 --- a/packages/reown_appkit/lib/modal/appkit_modal_impl.dart +++ b/packages/reown_appkit/lib/modal/appkit_modal_impl.dart @@ -1607,18 +1607,18 @@ class ReownAppKitModal if (_currentSession == null) { throw ReownAppKitModalException('Session is null'); } - if (!NamespaceUtils.isValidChainId(chainId)) { - throw Errors.getSdkError( - Errors.UNSUPPORTED_CHAINS, - context: 'chainId should conform to "CAIP-2" format', - ).toSignError(); - } // _appKit.core.logger.d( '[$runtimeType] request, chainId: $chainId, ' '${jsonEncode(request.toJson())}', ); try { + if (!NamespaceUtils.isValidChainId(chainId)) { + throw Errors.getSdkError( + Errors.UNSUPPORTED_CHAINS, + context: 'chainId should conform to "CAIP-2" format', + ).toSignError(); + } if (_currentSession!.sessionService.isMagic) { return await _magicService.request(chainId: chainId, request: request); } @@ -1656,11 +1656,12 @@ class ReownAppKitModal if (_isUserRejectedError(e)) { onModalError.broadcast(UserRejectedRequest()); } else if (e is CoinbaseServiceException) { - // If the error is due to no session on Coinbase Wallet we disconnnect the session on Modal. + // If the error is due to no session on Coinbase Wallet we disconnect the session on Modal. // This is the only way to detect a missing session since Coinbase Wallet is not sending any event. throw ReownAppKitModalException('Coinbase Wallet Error'); } else if (e is ReownSignError) { onModalError.broadcast(ModalError(e.message)); + return; } rethrow; } diff --git a/packages/reown_core/lib/relay_client/websocket/websocket_handler.dart b/packages/reown_core/lib/relay_client/websocket/websocket_handler.dart index f288f3e8..6e231eb3 100644 --- a/packages/reown_core/lib/relay_client/websocket/websocket_handler.dart +++ b/packages/reown_core/lib/relay_client/websocket/websocket_handler.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:reown_core/models/basic_models.dart'; import 'package:reown_core/relay_client/websocket/i_websocket_handler.dart'; import 'package:stream_channel/stream_channel.dart'; @@ -58,12 +59,16 @@ class WebSocketHandler implements IWebSocketHandler { onError: (error) { try { _inputController?.addError(error); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] inputController.addError failed: $e'); + } }, onDone: () { try { _inputController?.close(); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] inputController.close failed: $e'); + } }, ); @@ -72,17 +77,23 @@ class WebSocketHandler implements IWebSocketHandler { (data) { try { _socket?.sink.add(data); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] sink.add failed: $e'); + } }, onError: (error) { try { _socket?.sink.addError(error); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] sink.addError failed: $e'); + } }, onDone: () { try { _socket?.sink.close(); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] sink.close failed: $e'); + } }, ); @@ -100,6 +111,7 @@ class WebSocketHandler implements IWebSocketHandler { try { await _socket?.ready; } catch (e) { + await close(); throw ReownCoreError( code: -1, message: 'WebSocket connection failed: ${e.toString()}', @@ -112,24 +124,34 @@ class WebSocketHandler implements IWebSocketHandler { // Cancel subscriptions first to prevent writes to closed sinks try { await _inputSubscription?.cancel(); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] inputSubscription.cancel failed: $e'); + } try { await _outputSubscription?.cancel(); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] outputSubscription.cancel failed: $e'); + } _inputSubscription = null; _outputSubscription = null; try { await _socket?.sink.close(); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] socket.sink.close failed: $e'); + } // Close the controllers to prevent further messages and race conditions try { await _inputController?.close(); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] inputController.close failed: $e'); + } try { await _outputController?.close(); - } catch (_) {} + } catch (e) { + debugPrint('[WebSocketHandler] outputController.close failed: $e'); + } _inputController = null; _outputController = null;