diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f5c5b554..5f5e1642 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -27,12 +27,12 @@
-
+
-
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 06843223..84f7b061 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -55,15 +55,15 @@
processing
remote-notification
-
+
CFBundleURLTypes
CFBundleURLName
- nostr.scheme
+ mostro.scheme
CFBundleURLSchemes
- nostr
+ mostro
CFBundleTypeRole
Editor
diff --git a/lib/core/app.dart b/lib/core/app.dart
index ab2d85c3..1ce8b8d3 100644
--- a/lib/core/app.dart
+++ b/lib/core/app.dart
@@ -1,10 +1,13 @@
+import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
+import 'package:app_links/app_links.dart';
import 'package:mostro_mobile/core/app_routes.dart';
import 'package:mostro_mobile/core/app_theme.dart';
import 'package:mostro_mobile/core/deep_link_handler.dart';
+import 'package:mostro_mobile/core/deep_link_interceptor.dart';
import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart';
import 'package:mostro_mobile/generated/l10n.dart';
import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart';
@@ -27,15 +30,84 @@ class MostroApp extends ConsumerStatefulWidget {
class _MostroAppState extends ConsumerState {
GoRouter? _router;
bool _deepLinksInitialized = false;
+ DeepLinkInterceptor? _deepLinkInterceptor;
+ StreamSubscription? _customUrlSubscription;
@override
void initState() {
super.initState();
ref.read(lifecycleManagerProvider);
+ _initializeDeepLinkInterceptor();
+ _processInitialDeepLink();
+ }
+
+ /// Initialize the deep link interceptor
+ void _initializeDeepLinkInterceptor() {
+ _deepLinkInterceptor = DeepLinkInterceptor();
+ _deepLinkInterceptor!.initialize();
+
+ // Listen for intercepted custom URLs
+ _customUrlSubscription = _deepLinkInterceptor!.customUrlStream.listen(
+ (url) async {
+ debugPrint('Intercepted custom URL: $url');
+
+ // Process the URL through our deep link handler
+ if (_router != null) {
+ try {
+ final uri = Uri.parse(url);
+ final deepLinkHandler = ref.read(deepLinkHandlerProvider);
+ await deepLinkHandler.handleInitialDeepLink(uri, _router!);
+ } catch (e) {
+ debugPrint('Error handling intercepted URL: $e');
+ }
+ }
+ },
+ onError: (error) {
+ debugPrint('Error in custom URL stream: $error');
+ },
+ );
+ }
+
+ /// Process initial deep link before router initialization
+ Future _processInitialDeepLink() async {
+ try {
+ final appLinks = AppLinks();
+ final initialUri = await appLinks.getInitialLink();
+
+ if (initialUri != null && initialUri.scheme == 'mostro') {
+ // Store the initial mostro URL for later processing
+ // and prevent it from being passed to GoRouter
+ debugPrint('Initial mostro deep link detected: $initialUri');
+
+ // Schedule the deep link processing after the router is ready
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _handleInitialMostroLink(initialUri);
+ });
+ }
+ } catch (e) {
+ debugPrint('Error processing initial deep link: $e');
+ }
+ }
+
+ /// Handle initial mostro link after router is ready
+ Future _handleInitialMostroLink(Uri uri) async {
+ try {
+ // Wait for router to be ready
+ await Future.delayed(const Duration(milliseconds: 100));
+
+ if (_router != null) {
+ final deepLinkHandler = ref.read(deepLinkHandlerProvider);
+ await deepLinkHandler.handleInitialDeepLink(uri, _router!);
+ }
+ } catch (e) {
+ debugPrint('Error handling initial mostro link: $e');
+ }
}
@override
void dispose() {
+ _customUrlSubscription?.cancel();
+ _deepLinkInterceptor?.dispose();
// Deep link handler disposal is handled automatically by Riverpod
super.dispose();
}
@@ -75,6 +147,7 @@ class _MostroAppState extends ConsumerState {
try {
final deepLinkHandler = ref.read(deepLinkHandlerProvider);
deepLinkHandler.initialize(_router!);
+
_deepLinksInitialized = true;
} catch (e, stackTrace) {
// Log the error but don't set _deepLinksInitialized to true
diff --git a/lib/core/app_routes.dart b/lib/core/app_routes.dart
index 40031bfe..05f89cf1 100644
--- a/lib/core/app_routes.dart
+++ b/lib/core/app_routes.dart
@@ -23,10 +23,13 @@ import 'package:mostro_mobile/features/walkthrough/screens/walkthrough_screen.da
import 'package:mostro_mobile/features/walkthrough/providers/first_run_provider.dart';
import 'package:mostro_mobile/shared/widgets/navigation_listener_widget.dart';
import 'package:mostro_mobile/shared/widgets/notification_listener_widget.dart';
+import 'package:logger/logger.dart';
+import 'package:mostro_mobile/generated/l10n.dart';
GoRouter createRouter(WidgetRef ref) {
return GoRouter(
navigatorKey: GlobalKey(),
+ initialLocation: '/',
redirect: (context, state) {
final firstRunState = ref.read(firstRunProvider);
@@ -41,8 +44,31 @@ GoRouter createRouter(WidgetRef ref) {
error: (_, __) => null,
);
},
+ errorBuilder: (context, state) {
+ final logger = Logger();
+ logger.w('GoRouter error: ${state.error}');
+
+ // For errors, show a generic error page
+ return Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.error, size: 64),
+ const SizedBox(height: 16),
+ Text(S.of(context)!.deepLinkNavigationError(state.error.toString())),
+ const SizedBox(height: 16),
+ ElevatedButton(
+ onPressed: () => context.go('/'),
+ child: Text(S.of(context)!.deepLinkGoHome),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
routes: [
- ShellRoute(
+ ShellRoute(
builder: (BuildContext context, GoRouterState state, Widget child) {
return NotificationListenerWidget(
child: NavigationListenerWidget(
@@ -227,10 +253,6 @@ GoRouter createRouter(WidgetRef ref) {
],
),
],
- initialLocation: '/',
- errorBuilder: (context, state) => Scaffold(
- body: Center(child: Text(state.error.toString())),
- ),
);
}
diff --git a/lib/core/deep_link_handler.dart b/lib/core/deep_link_handler.dart
index aa24dad6..c729475a 100644
--- a/lib/core/deep_link_handler.dart
+++ b/lib/core/deep_link_handler.dart
@@ -31,6 +31,11 @@ class DeepLinkHandler {
);
}
+ /// Handles initial deep link from app launch
+ Future handleInitialDeepLink(Uri uri, GoRouter router) async {
+ await _handleDeepLink(uri, router);
+ }
+
/// Handles incoming deep links
Future _handleDeepLink(
Uri uri,
@@ -39,9 +44,9 @@ class DeepLinkHandler {
try {
_logger.i('Handling deep link: $uri');
- // Check if it's a nostr: scheme
- if (uri.scheme == 'nostr') {
- await _handleNostrDeepLink(uri.toString(), router);
+ // Check if it's a mostro: scheme
+ if (uri.scheme == 'mostro') {
+ await _handleMostroDeepLink(uri.toString(), router);
} else {
_logger.w('Unsupported deep link scheme: ${uri.scheme}');
final context = router.routerDelegate.navigatorKey.currentContext;
@@ -58,8 +63,8 @@ class DeepLinkHandler {
}
}
- /// Handles nostr: scheme deep links
- Future _handleNostrDeepLink(
+ /// Handles mostro: scheme deep links
+ Future _handleMostroDeepLink(
String url,
GoRouter router,
) async {
@@ -75,8 +80,8 @@ class DeepLinkHandler {
final nostrService = _ref.read(nostrServiceProvider);
final deepLinkService = _ref.read(deepLinkServiceProvider);
- // Process the nostr link
- final result = await deepLinkService.processNostrLink(url, nostrService);
+ // Process the mostro link
+ final result = await deepLinkService.processMostroLink(url, nostrService, context!);
// Get fresh context after async operation
final currentContext = router.routerDelegate.navigatorKey.currentContext;
@@ -87,8 +92,10 @@ class DeepLinkHandler {
}
if (result.isSuccess && result.orderInfo != null) {
- // Navigate to the appropriate screen
- deepLinkService.navigateToOrder(router, result.orderInfo!);
+ // Navigate to the appropriate screen with proper timing
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ deepLinkService.navigateToOrder(router, result.orderInfo!);
+ });
_logger.i('Successfully navigated to order: ${result.orderInfo!.orderId} (${result.orderInfo!.orderType.value})');
} else {
final errorContext = router.routerDelegate.navigatorKey.currentContext;
@@ -96,10 +103,10 @@ class DeepLinkHandler {
final errorMessage = result.error ?? S.of(errorContext)!.failedToLoadOrder;
_showErrorSnackBar(errorContext, errorMessage);
}
- _logger.w('Failed to process nostr link: ${result.error}');
+ _logger.w('Failed to process mostro link: ${result.error}');
}
} catch (e) {
- _logger.e('Error processing nostr deep link: $e');
+ _logger.e('Error processing mostro deep link: $e');
final errorContext = router.routerDelegate.navigatorKey.currentContext;
if (errorContext != null && errorContext.mounted) {
Navigator.of(errorContext).pop(); // Hide loading if still showing
diff --git a/lib/core/deep_link_interceptor.dart b/lib/core/deep_link_interceptor.dart
new file mode 100644
index 00000000..2a9862d4
--- /dev/null
+++ b/lib/core/deep_link_interceptor.dart
@@ -0,0 +1,57 @@
+import 'dart:async';
+import 'package:flutter/widgets.dart';
+import 'package:logger/logger.dart';
+
+/// A deep link interceptor that prevents custom schemes from reaching GoRouter
+/// This prevents assertion failures when the system tries to parse mostro: URLs
+class DeepLinkInterceptor extends WidgetsBindingObserver {
+ final Logger _logger = Logger();
+ final StreamController _customUrlController =
+ StreamController.broadcast();
+
+ /// Stream for custom URLs that were intercepted
+ Stream get customUrlStream => _customUrlController.stream;
+
+ /// Initialize the interceptor
+ void initialize() {
+ WidgetsBinding.instance.addObserver(this);
+ _logger.i('DeepLinkInterceptor initialized');
+ }
+
+ @override
+ Future didPushRouteInformation(RouteInformation routeInformation) async {
+ final uri = routeInformation.uri;
+ _logger.i('Route information received: $uri');
+
+ // Check if this is a custom scheme URL
+ if (_isCustomScheme(uri)) {
+ _logger.i('Custom scheme detected: ${uri.scheme}, intercepting');
+
+ // Emit the custom URL for processing
+ _customUrlController.add(uri.toString());
+
+ // Return true to indicate we handled this route, preventing it from
+ // reaching GoRouter and causing assertion failures
+ return true;
+ }
+
+ // Let normal URLs pass through to GoRouter
+ return super.didPushRouteInformation(routeInformation);
+ }
+
+ // Note: didPushRoute is deprecated, but we keep it for compatibility
+ // The main handling is done in didPushRouteInformation above
+
+ /// Check if the URI uses a custom scheme
+ bool _isCustomScheme(Uri uri) {
+ return uri.scheme == 'mostro' ||
+ (!uri.scheme.startsWith('http') && uri.scheme.isNotEmpty);
+ }
+
+ /// Dispose the interceptor
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(this);
+ _customUrlController.close();
+ _logger.i('DeepLinkInterceptor disposed');
+ }
+}
\ No newline at end of file
diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb
index 749e0ef8..dcaca68d 100644
--- a/lib/l10n/intl_en.arb
+++ b/lib/l10n/intl_en.arb
@@ -452,5 +452,21 @@
"unsupportedLinkFormat": "Unsupported link format",
"failedToOpenLink": "Failed to open link",
"failedToLoadOrder": "Failed to load order",
- "failedToOpenOrder": "Failed to open order"
+ "failedToOpenOrder": "Failed to open order",
+
+ "@_comment_deep_link_errors": "Deep Link Error Messages",
+ "deepLinkInvalidFormat": "Invalid mostro: URL format",
+ "deepLinkParseError": "Failed to parse mostro: URL",
+ "deepLinkInvalidOrderId": "Invalid order ID format",
+ "deepLinkNoRelays": "No relays specified in URL",
+ "deepLinkOrderNotFound": "Order not found or invalid",
+ "deepLinkNavigationError": "Navigation Error: {error}",
+ "@deepLinkNavigationError": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "deepLinkGoHome": "Go Home"
}
diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb
index 0edfbf22..f0274f22 100644
--- a/lib/l10n/intl_es.arb
+++ b/lib/l10n/intl_es.arb
@@ -485,6 +485,22 @@
"unsupportedLinkFormat": "Formato de enlace no compatible",
"failedToOpenLink": "Error al abrir enlace",
"failedToLoadOrder": "Error al cargar orden",
- "failedToOpenOrder": "Error al abrir orden"
+ "failedToOpenOrder": "Error al abrir orden",
+
+ "@_comment_deep_link_errors": "Mensajes de Error de Enlaces Profundos",
+ "deepLinkInvalidFormat": "Formato de URL mostro: inválido",
+ "deepLinkParseError": "Error al analizar la URL mostro:",
+ "deepLinkInvalidOrderId": "Formato de ID de orden inválido",
+ "deepLinkNoRelays": "No se especificaron relés en la URL",
+ "deepLinkOrderNotFound": "Orden no encontrada o inválida",
+ "deepLinkNavigationError": "Error de Navegación: {error}",
+ "@deepLinkNavigationError": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "deepLinkGoHome": "Ir al Inicio"
}
\ No newline at end of file
diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb
index 317ee750..18bf3dfc 100644
--- a/lib/l10n/intl_it.arb
+++ b/lib/l10n/intl_it.arb
@@ -493,6 +493,22 @@
"unsupportedLinkFormat": "Formato di link non supportato",
"failedToOpenLink": "Impossibile aprire il link",
"failedToLoadOrder": "Impossibile caricare l'ordine",
- "failedToOpenOrder": "Impossibile aprire l'ordine"
+ "failedToOpenOrder": "Impossibile aprire l'ordine",
+
+ "@_comment_deep_link_errors": "Messaggi di Errore per Deep Link",
+ "deepLinkInvalidFormat": "Formato URL mostro: non valido",
+ "deepLinkParseError": "Impossibile analizzare l'URL mostro:",
+ "deepLinkInvalidOrderId": "Formato ID ordine non valido",
+ "deepLinkNoRelays": "Nessun relay specificato nell'URL",
+ "deepLinkOrderNotFound": "Ordine non trovato o non valido",
+ "deepLinkNavigationError": "Errore di Navigazione: {error}",
+ "@deepLinkNavigationError": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "deepLinkGoHome": "Vai alla Home"
}
diff --git a/lib/services/deep_link_service.dart b/lib/services/deep_link_service.dart
index ec2179c3..cd0436ee 100644
--- a/lib/services/deep_link_service.dart
+++ b/lib/services/deep_link_service.dart
@@ -1,11 +1,14 @@
import 'dart:async';
import 'package:app_links/app_links.dart';
+import 'package:dart_nostr/dart_nostr.dart';
+import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:logger/logger.dart';
import 'package:mostro_mobile/data/models/enums/order_type.dart';
import 'package:mostro_mobile/services/nostr_service.dart';
import 'package:mostro_mobile/shared/utils/nostr_utils.dart';
+import 'package:mostro_mobile/generated/l10n.dart';
/// Contains order information extracted from a Nostr event
class OrderInfo {
@@ -23,7 +26,8 @@ class DeepLinkService {
final AppLinks _appLinks = AppLinks();
// Stream controller for deep link events
- final StreamController _deepLinkController = StreamController.broadcast();
+ final StreamController _deepLinkController =
+ StreamController.broadcast();
Stream get deepLinkStream => _deepLinkController.stream;
// Flag to track if service is initialized
@@ -45,13 +49,9 @@ class DeepLinkService {
},
);
- // Check for deep link when app is launched
- final initialUri = await _appLinks.getInitialLink();
- if (initialUri != null) {
- _logger.i('App launched with deep link: $initialUri');
- _handleDeepLink(initialUri);
- }
-
+ // NOTE: We don't process the initial link here to avoid GoRouter conflicts
+ // The initial link will be handled by the app initialization in app.dart
+
_isInitialized = true;
_logger.i('DeepLinkService initialized successfully');
} catch (e) {
@@ -65,68 +65,119 @@ class DeepLinkService {
_deepLinkController.add(uri);
}
- /// Processes a nostr: deep link and resolves order information
- Future processNostrLink(
+ /// Processes a mostro: deep link and resolves order information
+ Future processMostroLink(
String url,
NostrService nostrService,
+ BuildContext context,
) async {
try {
- _logger.i('Processing nostr link: $url');
+ _logger.i('Processing mostro link: $url');
// Validate URL format
- if (!NostrUtils.isValidNostrUrl(url)) {
- return DeepLinkResult.error('Invalid nostr: URL format');
+ if (!NostrUtils.isValidMostroUrl(url)) {
+ return DeepLinkResult.error(S.of(context)!.deepLinkInvalidFormat);
+ }
+
+ // Parse the mostro URL
+ final orderInfo = NostrUtils.parseMostroUrl(url);
+ if (orderInfo == null) {
+ return DeepLinkResult.error(S.of(context)!.deepLinkParseError);
}
- // Parse the nevent
- final eventInfo = NostrUtils.parseNostrUrl(url);
- if (eventInfo == null) {
- return DeepLinkResult.error('Failed to parse nostr: URL');
+ final orderId = orderInfo['orderId'] as String;
+ final relays = orderInfo['relays'] as List;
+
+ // Validate order ID format (UUID-like string)
+ if (orderId.isEmpty || orderId.length < 10) {
+ return DeepLinkResult.error(S.of(context)!.deepLinkInvalidOrderId);
}
- final eventId = eventInfo['eventId'] as String;
- final relays = eventInfo['relays'] as List;
+ // Validate relays
+ if (relays.isEmpty) {
+ return DeepLinkResult.error(S.of(context)!.deepLinkNoRelays);
+ }
- _logger.i('Parsed event ID: $eventId, relays: $relays');
+ _logger.i('Parsed order ID: $orderId, relays: $relays');
- // Fetch the order info from the event's tags
- final orderInfo = await _fetchOrderInfoFromEvent(
- eventId,
+ // Fetch the order info directly using the order ID
+ final fetchedOrderInfo = await _fetchOrderInfoById(
+ orderId,
relays,
nostrService,
);
- if (orderInfo == null) {
- return DeepLinkResult.error('Order not found or invalid');
+ if (fetchedOrderInfo == null) {
+ if (context.mounted) {
+ return DeepLinkResult.error(S.of(context)!.deepLinkOrderNotFound);
+ } else {
+ return DeepLinkResult.error('Order not found or invalid');
+ }
}
- return DeepLinkResult.success(orderInfo);
+ return DeepLinkResult.success(fetchedOrderInfo);
} catch (e) {
- _logger.e('Error processing nostr link: $e');
+ _logger.e('Error processing mostro link: $e');
return DeepLinkResult.error('Failed to process deep link: $e');
}
}
- /// Fetches order information from the specified event's tags
- Future _fetchOrderInfoFromEvent(
- String eventId,
+ /// Fetches order information using the order ID by searching for NIP-69 events with 'd' tag
+ Future _fetchOrderInfoById(
+ String orderId,
List relays,
NostrService nostrService,
) async {
try {
+ // Create a filter to search for NIP-69 order events with the specific order ID
+ final filter = NostrFilter(
+ kinds: [38383], // NIP-69 order events
+ additionalFilters: {
+ '#d': [orderId]
+ }, // Order ID is stored in 'd' tag
+ );
+
+ List events = [];
+
// First try to fetch from specified relays
if (relays.isNotEmpty) {
- final orderInfo = await nostrService.fetchOrderInfoByEventId(eventId, relays);
- if (orderInfo != null) {
- return orderInfo;
+ // Use the specific relays from the deep link URL
+ final orderEvents = await nostrService.fetchEvents(filter, specificRelays: relays);
+ events.addAll(orderEvents);
+ }
+
+ // If no events found and we have default relays, try those
+ if (events.isEmpty) {
+ _logger.i('Order not found in specified relays, trying default relays');
+ final defaultEvents = await nostrService.fetchEvents(filter);
+ events.addAll(defaultEvents);
+ }
+
+ // Process the first matching event
+ if (events.isNotEmpty) {
+ final event = events.first;
+
+ // Extract order type from 'k' tag
+ final kTag = event.tags?.firstWhere(
+ (tag) => tag.isNotEmpty && tag[0] == 'k',
+ orElse: () => [],
+ );
+
+ if (kTag != null && kTag.length > 1) {
+ final orderTypeValue = kTag[1];
+ final orderType =
+ orderTypeValue == 'sell' ? OrderType.sell : OrderType.buy;
+
+ return OrderInfo(
+ orderId: orderId,
+ orderType: orderType,
+ );
}
}
- // Fallback: try fetching from default relays
- _logger.i('Event not found in specified relays, trying default relays');
- return await nostrService.fetchOrderInfoByEventId(eventId);
+ return null;
} catch (e) {
- _logger.e('Error fetching order info from event: $e');
+ _logger.e('Error fetching order info by ID: $e');
return null;
}
}
@@ -145,8 +196,30 @@ class DeepLinkService {
/// Navigates to the appropriate screen for the given order
void navigateToOrder(GoRouter router, OrderInfo orderInfo) {
final route = getNavigationRoute(orderInfo);
- _logger.i('Navigating to: $route (Order: ${orderInfo.orderId}, Type: ${orderInfo.orderType.value})');
- router.push(route);
+ _logger.i(
+ 'Navigating to: $route (Order: ${orderInfo.orderId}, Type: ${orderInfo.orderType.value})');
+
+ // Use post-frame callback to ensure navigation happens after the current frame
+ // This prevents GoRouter assertion failures during app lifecycle transitions
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ try {
+ // Validate that the router is still in a valid state before navigation
+ final context = router.routerDelegate.navigatorKey.currentContext;
+ if (context != null && context.mounted) {
+ router.push(route);
+ } else {
+ _logger.w('Router context is not available for navigation to: $route');
+ }
+ } catch (e) {
+ _logger.e('Error navigating to order: $e');
+ // Fallback: try using go instead of push if push fails
+ try {
+ router.go(route);
+ } catch (fallbackError) {
+ _logger.e('Fallback navigation also failed: $fallbackError');
+ }
+ }
+ });
}
/// Cleans up resources
diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart
index 575eb83f..d53e4f3d 100644
--- a/lib/services/nostr_service.dart
+++ b/lib/services/nostr_service.dart
@@ -89,11 +89,17 @@ class NostrService {
}
}
- Future> fecthEvents(NostrFilter filter) async {
+ Future> fetchEvents(NostrFilter filter, {List? specificRelays}) async {
if (!_isInitialized) {
throw Exception('Nostr is not initialized. Call init() first.');
}
+ // If specific relays are provided, use the relay-specific fetching logic
+ if (specificRelays != null && specificRelays.isNotEmpty) {
+ return await _fetchFromSpecificRelays(filter, specificRelays);
+ }
+
+ // Default behavior: use all configured relays
final request = NostrRequest(filters: [filter]);
return await _nostr.services.relays.startEventsSubscriptionAsync(
request: request,
@@ -221,7 +227,7 @@ class NostrService {
events = await _fetchFromSpecificRelays(filter, specificRelays);
} else {
// Use default relays
- events = await fecthEvents(filter);
+ events = await fetchEvents(filter);
}
if (events.isEmpty) {
@@ -256,8 +262,9 @@ class NostrService {
}
/// Fetches order information from an event by extracting the 'd' tag (order ID) and 'k' tag (order type)
- /// This is specifically for deep link handling where the nevent points to an event containing order information
- Future fetchOrderInfoByEventId(String eventId, [List? specificRelays]) async {
+ /// This is specifically for deep link handling where the mostro: URL provides order information
+ Future fetchOrderInfoByEventId(String eventId,
+ [List? specificRelays]) async {
try {
_logger.i('Fetching order ID from event: $eventId');
@@ -273,7 +280,7 @@ class NostrService {
events = await _fetchFromSpecificRelays(filter, specificRelays);
} else {
// Use default relays
- events = await fecthEvents(filter);
+ events = await fetchEvents(filter);
}
if (events.isEmpty) {
@@ -333,7 +340,8 @@ class NostrService {
return null;
}
- _logger.i('Successfully extracted order info - ID: $dTag, Type: ${orderType.value} from event: $eventId');
+ _logger.i(
+ 'Successfully extracted order info - ID: $dTag, Type: ${orderType.value} from event: $eventId');
return OrderInfo(orderId: dTag, orderType: orderType);
} catch (e) {
_logger.e('Error fetching order ID from event: $e');
@@ -368,7 +376,7 @@ class NostrService {
await updateSettings(tempSettings);
// Fetch the events
- final events = await fecthEvents(filter);
+ final events = await fetchEvents(filter);
// Restore original relays
await updateSettings(settings);
@@ -376,7 +384,7 @@ class NostrService {
return events;
} else {
// No new relays to add, use normal fetch
- return await fecthEvents(filter);
+ return await fetchEvents(filter);
}
} catch (e) {
_logger.e('Error fetching from specific relays: $e');
diff --git a/lib/shared/utils/nostr_utils.dart b/lib/shared/utils/nostr_utils.dart
index cad6b8f3..c6f2c222 100644
--- a/lib/shared/utils/nostr_utils.dart
+++ b/lib/shared/utils/nostr_utils.dart
@@ -108,89 +108,63 @@ class NostrUtils {
return _instance.services.bech32.encodeBech32(hrp, data);
}
- /// Decodes a nevent string (NIP-19) to extract event ID and relay information
- /// Returns a map with 'eventId' and 'relays' keys
- static Map decodeNevent(String nevent) {
- if (!nevent.startsWith('nevent1')) {
- throw ArgumentError('Invalid nevent format: must start with nevent1');
- }
+ /// Validates if a string is a valid mostro: URL
+ /// Format: mostro:order-id&relays=wss://relay1,wss://relay2
+ static bool isValidMostroUrl(String url) {
+ if (!url.startsWith('mostro:')) return false;
try {
- final decoded = _instance.services.bech32.decodeBech32(nevent);
- if (decoded.isEmpty || decoded.length < 2) {
- throw FormatException('Invalid bech32 decoding result');
- }
-
- final data = decoded[0]; // Get the data part (index 0)
- final bytes = hex.decode(data);
-
- final result = {
- 'eventId': '',
- 'relays': [],
- };
-
- // Parse TLV (Type-Length-Value) format
- int index = 0;
- while (index < bytes.length) {
- if (index + 2 >= bytes.length) break;
-
- final type = bytes[index];
- final length = bytes[index + 1];
-
- if (index + 2 + length > bytes.length) break;
-
- final value = bytes.sublist(index + 2, index + 2 + length);
-
- switch (type) {
- case 0: // Event ID (32 bytes)
- if (length == 32) {
- result['eventId'] = hex.encode(value);
- }
- break;
- case 1: // Relay URL (variable length)
- final relayUrl = utf8.decode(value);
- if (relayUrl.isNotEmpty) {
- (result['relays'] as List).add(relayUrl);
- }
- break;
- // Skip other types for now
+ final uri = Uri.parse(url);
+ if (uri.scheme != 'mostro') return false;
+
+ // Check if we have an order ID (path)
+ final orderId = uri.path;
+ if (orderId.isEmpty) return false;
+
+ // Check if relays parameter exists
+ final relaysParam = uri.queryParameters['relays'];
+ if (relaysParam == null || relaysParam.isEmpty) return false;
+
+ // Validate relay URLs
+ final relays = relaysParam.split(',');
+ for (final relay in relays) {
+ final trimmedRelay = relay.trim();
+ if (!trimmedRelay.startsWith('wss://') &&
+ !trimmedRelay.startsWith('ws://')) {
+ return false;
}
-
- index += 2 + length;
}
- if ((result['eventId'] as String).isEmpty) {
- throw FormatException('No event ID found in nevent');
- }
-
- return result;
+ return true;
} catch (e) {
- throw FormatException('Failed to decode nevent: $e');
+ return false;
}
}
- /// Validates if a string is a valid nostr: URL
- static bool isValidNostrUrl(String url) {
- if (!url.startsWith('nostr:')) return false;
-
- final nevent = url.substring(6); // Remove 'nostr:' prefix
+ /// Parses a mostro: URL and returns order information
+ /// Format: mostro:order-id&relays=wss://relay1,wss://relay2
+ /// Returns a map with 'orderId' and 'relays' keys
+ static Map? parseMostroUrl(String url) {
+ if (!isValidMostroUrl(url)) return null;
try {
- decodeNevent(nevent);
- return true;
- } catch (e) {
- return false;
- }
- }
+ final uri = Uri.parse(url);
- /// Parses a nostr: URL and returns event information
- static Map? parseNostrUrl(String url) {
- if (!isValidNostrUrl(url)) return null;
+ final orderId = uri.path;
+ final relaysParam = uri.queryParameters['relays'];
- final nevent = url.substring(6); // Remove 'nostr:' prefix
+ if (orderId.isEmpty || relaysParam == null) return null;
- try {
- return decodeNevent(nevent);
+ final relays = relaysParam
+ .split(',')
+ .map((relay) => relay.trim())
+ .where((relay) => relay.isNotEmpty)
+ .toList();
+
+ return {
+ 'orderId': orderId,
+ 'relays': relays,
+ };
} catch (e) {
return null;
}
diff --git a/test/services/mostro_service_test.mocks.dart b/test/services/mostro_service_test.mocks.dart
index 3d371da9..da25fcd0 100644
--- a/test/services/mostro_service_test.mocks.dart
+++ b/test/services/mostro_service_test.mocks.dart
@@ -179,11 +179,15 @@ class MockNostrService extends _i1.Mock implements _i6.NostrService {
) as _i7.Future);
@override
- _i7.Future> fecthEvents(_i3.NostrFilter? filter) =>
+ _i7.Future> fetchEvents(
+ _i3.NostrFilter? filter, {
+ List? specificRelays,
+ }) =>
(super.noSuchMethod(
Invocation.method(
- #fecthEvents,
+ #fetchEvents,
[filter],
+ {#specificRelays: specificRelays},
),
returnValue: _i7.Future>.value(<_i3.NostrEvent>[]),
) as _i7.Future>);
diff --git a/test/utils/nostr_utils_test.dart b/test/utils/nostr_utils_test.dart
index c692dbaa..b5482fdb 100644
--- a/test/utils/nostr_utils_test.dart
+++ b/test/utils/nostr_utils_test.dart
@@ -2,33 +2,56 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mostro_mobile/shared/utils/nostr_utils.dart';
void main() {
- group('NostrUtils nevent decoding tests', () {
- const testNevent = 'nevent1qqs9hcuev4z4frqnn5wagnupzg4dahcwkyy9r3xvsjw6wd7cm4tmh4gprfmhxue69uhhyetvv9ujumt0wd68ymewdejhgam0wf4scqjs3l';
- const testUrl = 'nostr:$testNevent';
- test('should decode nevent and extract event ID', () {
- final result = NostrUtils.decodeNevent(testNevent);
+ group('NostrUtils mostro URL tests', () {
+ const testOrderId = 'order123456';
+ const testRelays = 'wss://relay.mostro.network,wss://relay.damus.io';
+ const testMostroUrl = 'mostro:$testOrderId?relays=$testRelays';
+
+ test('should validate mostro URL correctly', () {
+ expect(NostrUtils.isValidMostroUrl(testMostroUrl), isTrue);
+ });
+
+ test('should reject invalid mostro URLs', () {
+ expect(NostrUtils.isValidMostroUrl('mostro:'), isFalse);
+ expect(NostrUtils.isValidMostroUrl('mostro:order123'), isFalse); // no relays
+ expect(NostrUtils.isValidMostroUrl('mostro:?relays=wss://relay.com'), isFalse); // no order id
+ expect(NostrUtils.isValidMostroUrl('nostr:order123?relays=wss://relay.com'), isFalse); // wrong scheme
+ expect(NostrUtils.isValidMostroUrl('mostro:order123?relays=http://relay.com'), isFalse); // invalid relay protocol
+ });
+
+ test('should parse mostro URL correctly', () {
+ final result = NostrUtils.parseMostroUrl(testMostroUrl);
- expect(result, isA