From 53839636471004609a40a874319d5a396951862c Mon Sep 17 00:00:00 2001 From: Taksh Date: Fri, 31 Jul 2026 13:34:42 +0300 Subject: [PATCH 1/2] fix(mobile): explain missing legacy /pair pairing endpoints Map WebSocket 404 failures on /pair to operator-facing guidance about BUZZ_PAIRING_RELAY_URL and buzz-pair-relay instead of a bare transport error. Signed-off-by: Taksh --- .../features/pairing/pairing_provider.dart | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/mobile/lib/features/pairing/pairing_provider.dart b/mobile/lib/features/pairing/pairing_provider.dart index 6a6c57a673..6a9d38c203 100644 --- a/mobile/lib/features/pairing/pairing_provider.dart +++ b/mobile/lib/features/pairing/pairing_provider.dart @@ -65,6 +65,27 @@ typedef PairingSocketFactory = required void Function(Object? error) onDisconnected, }); + +String pairingEndpointNotFoundMessage(String relayUrl) { + final sanitized = relayUrl.trim(); + if (sanitized.isEmpty) { + return 'Pairing endpoint not found (404). The relay advertises NIP-43 without ' + 'a working /pair route. Ask the operator to set BUZZ_PAIRING_RELAY_URL or ' + 'route /pair to buzz-pair-relay, then create a new QR code on desktop.'; + } + return 'Pairing endpoint $sanitized not found (404). The relay advertises NIP-43 ' + 'without a working /pair route. Ask the operator to set BUZZ_PAIRING_RELAY_URL ' + 'or route /pair to buzz-pair-relay, then create a new QR code on desktop.'; +} + +bool pairingLooksLikeMissingLegacyEndpoint(Object error, {String? relayUrl}) { + final message = error.toString().toLowerCase(); + final mentions404 = message.contains('404') || message.contains('not found'); + final legacyPath = relayUrl?.contains('/pair') == true || message.contains('/pair'); + return mentions404 && legacyPath; +} + + class PairingNotifier extends Notifier { final PairingSocketFactory _socketFactory; PairingSocket? _socket; @@ -168,6 +189,7 @@ class PairingNotifier extends Notifier { Future _pairNipAb(String uri) async { state = const PairingState(status: PairingStatus.connecting); + String? pairingRelayUrl; try { // 1. Parse the nostrpair:// URI. @@ -175,7 +197,8 @@ class PairingNotifier extends Notifier { _sourcePubkey = qr.sourcePubkey; _sessionSecret = qr.sessionSecret; - final relayWsUrl = qr.relays.first; + pairingRelayUrl = qr.relays.first; + final relayWsUrl = pairingRelayUrl!; // 2. Generate ephemeral keypair. final keychain = nostr.Keys.generate(); @@ -258,13 +281,16 @@ class PairingNotifier extends Notifier { _cleanup(); state = PairingState( status: PairingStatus.error, - errorMessage: _friendlyErrorMessage(e), + errorMessage: _friendlyErrorMessage(e, relayUrl: pairingRelayUrl), ); } } - static String _friendlyErrorMessage(Object error) { + static String _friendlyErrorMessage(Object error, {String? relayUrl}) { final message = error.toString(); + if (pairingLooksLikeMissingLegacyEndpoint(error, relayUrl: relayUrl)) { + return pairingEndpointNotFoundMessage(relayUrl ?? ''); + } if (message.contains('SocketException') || message.contains('Connection refused') || message.contains('Network is unreachable') || From 8d6b895f3eb652f785a4d02c0f94da5a83ed5d50 Mon Sep 17 00:00:00 2001 From: Taksh Date: Fri, 31 Jul 2026 13:34:42 +0300 Subject: [PATCH 2/2] test(mobile): cover legacy /pair pairing error detection Assert the helper flags bare 404 transport failures on /pair and surfaces BUZZ_PAIRING_RELAY_URL guidance for operators. Signed-off-by: Taksh --- .../pairing/pairing_provider_test.dart | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/mobile/test/features/pairing/pairing_provider_test.dart b/mobile/test/features/pairing/pairing_provider_test.dart index 6f49f71921..97a0fd2b9a 100644 --- a/mobile/test/features/pairing/pairing_provider_test.dart +++ b/mobile/test/features/pairing/pairing_provider_test.dart @@ -24,6 +24,32 @@ import 'package:buzz/shared/auth/auth.dart'; /// guards (private IPs, non-http schemes). /// - `reset()` returning to idle from an error state. void main() { + group('pairing endpoint diagnostics', () { + test('pairingEndpointNotFoundMessage mentions operator levers', () { + expect( + pairingEndpointNotFoundMessage('wss://relay.example/pair'), + contains('BUZZ_PAIRING_RELAY_URL'), + ); + }); + + test('pairingLooksLikeMissingLegacyEndpoint detects /pair 404s', () { + expect( + pairingLooksLikeMissingLegacyEndpoint( + Exception('WebSocket connection failed: HTTP error: 404 Not Found'), + relayUrl: 'wss://relay.example/pair', + ), + isTrue, + ); + expect( + pairingLooksLikeMissingLegacyEndpoint( + Exception('Connection refused'), + relayUrl: 'wss://relay.example/pair', + ), + isFalse, + ); + }); + }); + group('PairingNotifier', () { late ProviderContainer container; late FakeAuthNotifier fakeAuth;