Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions mobile/lib/features/pairing/pairing_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<PairingState> {
final PairingSocketFactory _socketFactory;
PairingSocket? _socket;
Expand Down Expand Up @@ -168,14 +189,16 @@ class PairingNotifier extends Notifier<PairingState> {

Future<void> _pairNipAb(String uri) async {
state = const PairingState(status: PairingStatus.connecting);
String? pairingRelayUrl;

try {
// 1. Parse the nostrpair:// URI.
final qr = parseNostrpairUri(uri);
_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();
Expand Down Expand Up @@ -258,13 +281,16 @@ class PairingNotifier extends Notifier<PairingState> {
_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') ||
Expand Down
26 changes: 26 additions & 0 deletions mobile/test/features/pairing/pairing_provider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down