Skip to content
Merged
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
4 changes: 2 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!-- Deep Link Support for nostr: scheme -->
<!-- Deep Link Support for mostro: scheme -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostr" />
<data android:scheme="mostro" />
</intent-filter>
</activity>

Expand Down
6 changes: 3 additions & 3 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@
<string>processing</string>
<string>remote-notification</string>
</array>
<!-- Deep Link Support for nostr: scheme -->
<!-- Deep Link Support for mostro: scheme -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>nostr.scheme</string>
<string>mostro.scheme</string>
<key>CFBundleURLSchemes</key>
<array>
<string>nostr</string>
<string>mostro</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
Expand Down
73 changes: 73 additions & 0 deletions lib/core/app.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -27,15 +30,84 @@ class MostroApp extends ConsumerStatefulWidget {
class _MostroAppState extends ConsumerState<MostroApp> {
GoRouter? _router;
bool _deepLinksInitialized = false;
DeepLinkInterceptor? _deepLinkInterceptor;
StreamSubscription<String>? _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<void> _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<void> _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();
}
Expand Down Expand Up @@ -75,6 +147,7 @@ class _MostroAppState extends ConsumerState<MostroApp> {
try {
final deepLinkHandler = ref.read(deepLinkHandlerProvider);
deepLinkHandler.initialize(_router!);

_deepLinksInitialized = true;
} catch (e, stackTrace) {
// Log the error but don't set _deepLinksInitialized to true
Expand Down
32 changes: 27 additions & 5 deletions lib/core/app_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<NavigatorState>(),
initialLocation: '/',
redirect: (context, state) {
final firstRunState = ref.read(firstRunProvider);

Expand All @@ -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(
Expand Down Expand Up @@ -227,10 +253,6 @@ GoRouter createRouter(WidgetRef ref) {
],
),
],
initialLocation: '/',
errorBuilder: (context, state) => Scaffold(
body: Center(child: Text(state.error.toString())),
),
);
}

Expand Down
29 changes: 18 additions & 11 deletions lib/core/deep_link_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ class DeepLinkHandler {
);
}

/// Handles initial deep link from app launch
Future<void> handleInitialDeepLink(Uri uri, GoRouter router) async {
await _handleDeepLink(uri, router);
}

/// Handles incoming deep links
Future<void> _handleDeepLink(
Uri uri,
Expand All @@ -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;
Expand All @@ -58,8 +63,8 @@ class DeepLinkHandler {
}
}

/// Handles nostr: scheme deep links
Future<void> _handleNostrDeepLink(
/// Handles mostro: scheme deep links
Future<void> _handleMostroDeepLink(
String url,
GoRouter router,
) async {
Expand All @@ -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;
Expand All @@ -87,19 +92,21 @@ 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;
if (errorContext != null && errorContext.mounted) {
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
Expand Down
57 changes: 57 additions & 0 deletions lib/core/deep_link_interceptor.dart
Original file line number Diff line number Diff line change
@@ -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<String> _customUrlController =
StreamController<String>.broadcast();

/// Stream for custom URLs that were intercepted
Stream<String> get customUrlStream => _customUrlController.stream;

/// Initialize the interceptor
void initialize() {
WidgetsBinding.instance.addObserver(this);
_logger.i('DeepLinkInterceptor initialized');
}

@override
Future<bool> 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');
}
}
18 changes: 17 additions & 1 deletion lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
18 changes: 17 additions & 1 deletion lib/l10n/intl_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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"

}
Loading