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
22 changes: 7 additions & 15 deletions mobile-app/lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:resonance_network_wallet/shared/global_navigator_key.dart';
import 'package:resonance_network_wallet/wallet_initializer.dart';
import 'package:resonance_network_wallet/v2/screens/auth/auth_wrapper.dart';
import 'package:resonance_network_wallet/v2/theme/app_theme.dart';
Expand All @@ -25,14 +24,13 @@ class _ResonanceWalletAppState extends ConsumerState<ResonanceWalletApp> {
void initState() {
super.initState();

WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(notificationIntegrationServiceProvider);
ref.read(deepLinkServiceProvider).init(navigatorKey);
ref.read(localNotificationsServiceProvider).setupNotificationsClickListener(navigatorKey);
ref.read(localNotificationsServiceProvider).handleLaunchByNotification(navigatorKey);
ref.read(notificationIntegrationServiceProvider);
ref.read(deepLinkServiceProvider).init();
final localNotifications = ref.read(localNotificationsServiceProvider);
localNotifications.setupNotificationsClickListener();
localNotifications.handleLaunchByNotification();

if (Platform.isAndroid) _referralService.checkPlayStoreReferralCode();
});
if (Platform.isAndroid) _referralService.checkPlayStoreReferralCode();
}

@override
Expand All @@ -45,15 +43,9 @@ class _ResonanceWalletAppState extends ConsumerState<ResonanceWalletApp> {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Quantus Wallet',
navigatorKey: navigatorKey,
navigatorObservers: [TelemetryNavigatorObserver()],
initialRoute: '/',
routes: {
'/': (context) => const WalletInitializer(),
// These routes are for deep linking, each will carry an intent
'/account': (context) => const WalletInitializer(),
'/transactions': (context) => const WalletInitializer(),
},
routes: {'/': (context) => const WalletInitializer()},
theme: AppTheme.darkTheme(context),
darkTheme: AppTheme.darkTheme(context),
themeMode: ThemeMode.dark,
Expand Down
8 changes: 1 addition & 7 deletions mobile-app/lib/providers/remote_config_provider.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/firebase_options.dart';
import 'package:resonance_network_wallet/services/remote_config_service.dart';
import 'package:resonance_network_wallet/services/firebase_messaging_service.dart';
import 'package:resonance_network_wallet/shared/global_navigator_key.dart';

final remoteConfigServiceProvider = Provider<RemoteConfigService>((ref) {
return RemoteConfigService();
Expand Down Expand Up @@ -68,11 +66,7 @@ class RemoteConfigNotifier extends StateNotifier<RemoteConfigModel> {

final fcmService = ref.read(firebaseMessagingServiceProvider);
await fcmService.init(); // This requests notification permission.

// Ensure navigatorKey.currentState is attached before handling any initial message.
WidgetsBinding.instance.addPostFrameCallback((_) {
fcmService.setupNotificationTapHandlers(navigatorKey);
});
fcmService.setupNotificationTapHandlers();

_isEnablingRemoteNotifications = false;
}
Expand Down
9 changes: 4 additions & 5 deletions mobile-app/lib/services/deep_link_service.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:app_links/app_links.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:resonance_network_wallet/providers/account_associations_providers.dart';
Expand All @@ -17,22 +16,22 @@ class DeepLinkService {
final _appLinks = AppLinks();
StreamSubscription<Uri>? _linkSubscription;

Future<void> init(GlobalKey<NavigatorState> navigatorKey) async {
Future<void> init() async {
// Handle links when the app is already open (warm state)
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
print('Received link while app is open: $uri');
_handleLink(uri, navigatorKey);
_handleLink(uri);
});

// Handle the link that opened the app (cold state)
final initialUri = await _appLinks.getInitialLink();
if (initialUri != null) {
print('Received initial link: $initialUri');
_handleLink(initialUri, navigatorKey);
_handleLink(initialUri);
}
}

void _handleLink(Uri uri, GlobalKey<NavigatorState> navigatorKey) {
void _handleLink(Uri uri) {
if (uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'account') {
String? accountId;

Expand Down
23 changes: 12 additions & 11 deletions mobile-app/lib/services/firebase_messaging_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class FirebaseMessagingService {
final SenotiService _senotiService = SenotiService();

bool _isInitialized = false;
bool __hasRegisteredHandlers = false;
bool _hasRegisteredHandlers = false;
String? _cachedToken;

FirebaseMessagingService(this._ref);
Expand Down Expand Up @@ -106,6 +106,7 @@ class FirebaseMessagingService {
}
try {
await _senotiService.unregisterDevice(token, _platform);
_cachedToken = null;
} catch (e) {
debugPrint('Failed to unregister device: $e');
}
Expand Down Expand Up @@ -146,33 +147,33 @@ class FirebaseMessagingService {
}

/// Handle the user tapping on an FCM notification that launched/resumed the app.
/// Call this after the navigator key is available.
void setupNotificationTapHandlers(GlobalKey<NavigatorState> navigatorKey) {
if (__hasRegisteredHandlers) return;
__hasRegisteredHandlers = true;
void setupNotificationTapHandlers() {
if (_hasRegisteredHandlers) return;
_hasRegisteredHandlers = true;

FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
debugPrint('FCM notification tapped (background): ${message.messageId}');
_handleNotificationTap(message, navigatorKey);
_handleNotificationTap(message);
});

_handleInitialMessage(navigatorKey);
_handleInitialMessage();
}

Future<void> _handleInitialMessage(GlobalKey<NavigatorState> navigatorKey) async {
Future<void> _handleInitialMessage() async {
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
debugPrint('FCM initial message (terminated): ${initialMessage.messageId}');
_handleNotificationTap(initialMessage, navigatorKey);
_handleNotificationTap(initialMessage);
}
}

void _handleNotificationTap(RemoteMessage message, GlobalKey<NavigatorState> navigatorKey) {
void _handleNotificationTap(RemoteMessage message) {
final data = message.data;

if (data.isEmpty) return;

final txService = _ref.read(transactionServiceProvider);
txService.navigateToTransactionFromPayloadIfPossible(data, navigatorKey);
txService.navigateToTransactionFromPayloadIfPossible(data);
Comment thread
cursor[bot] marked this conversation as resolved.
}

NotificationData? _remoteMessageToNotificationData(RemoteMessage message) {
Expand Down
34 changes: 26 additions & 8 deletions mobile-app/lib/services/local_notifications_service.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:resonance_network_wallet/models/notification_models.dart';
import 'package:resonance_network_wallet/services/telemetry_service.dart';
import 'package:resonance_network_wallet/services/transaction_service.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;
Expand Down Expand Up @@ -69,17 +70,25 @@ class LocalNotificationsService {
}

// This is for handling when app is in terminated state then launched by tapping notification.
Future<void> handleLaunchByNotification(GlobalKey<NavigatorState> navigatorKey) async {
Future<void> handleLaunchByNotification() async {
final notificationAppLaunchDetails = await _notificationPlugin.getNotificationAppLaunchDetails();
if (notificationAppLaunchDetails?.didNotificationLaunchApp != true) return;

final payload = notificationAppLaunchDetails!.notificationResponse?.payload;
if (payload == null || payload.isEmpty) return;

final txService = _ref.read(transactionServiceProvider);
final json = jsonDecode(payload);

txService.navigateToTransactionFromPayloadIfPossible(json, navigatorKey);
try {
final json = jsonDecode(payload);
txService.navigateToTransactionFromPayloadIfPossible(json);
} catch (e) {
debugPrint('Error decoding payload handle launch by notification: $e');
TelemetryService().sendError(
Comment thread
cursor[bot] marked this conversation as resolved.
'Error decoding notification launch payload',
error: e.runtimeType.toString(),
stackTrace: StackTrace.current,
);
}
}

Future<void> _showNotification(NotificationData notification) async {
Expand Down Expand Up @@ -125,14 +134,23 @@ class LocalNotificationsService {
}
}

void setupNotificationsClickListener(GlobalKey<NavigatorState> navigatorKey) {
void setupNotificationsClickListener() {
_onNotificationClick.stream.listen((payload) {
if (payload == null || payload.isEmpty) return;

final txService = _ref.read(transactionServiceProvider);
final json = jsonDecode(payload);

txService.navigateToTransactionFromPayloadIfPossible(json, navigatorKey);
try {
final json = jsonDecode(payload);
txService.navigateToTransactionFromPayloadIfPossible(json);
} catch (e) {
debugPrint('Error decoding payload setup notifications click listener: $e');
TelemetryService().sendError(
'Error decoding notification click payload',
error: e.runtimeType.toString(),
stackTrace: StackTrace.current,
);
}
});
}

Expand Down
13 changes: 9 additions & 4 deletions mobile-app/lib/services/transaction_service.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:quantus_sdk/quantus_sdk.dart';
import 'package:resonance_network_wallet/models/transaction_role.dart';
import 'package:resonance_network_wallet/providers/account_providers.dart';
import 'package:resonance_network_wallet/providers/route_intent_providers.dart';
import 'package:resonance_network_wallet/services/telemetry_service.dart';

final transactionServiceProvider = Provider<TransactionService>((ref) {
return TransactionService(ref);
Expand Down Expand Up @@ -89,12 +90,11 @@ class TransactionService {
}
}

void navigateToTransactionFromPayloadIfPossible(Map<String, dynamic>? json, GlobalKey<NavigatorState> navigatorKey) {
void navigateToTransactionFromPayloadIfPossible(Map<String, dynamic> json) {
final event = deserializeTxEventFromJsonIfPossible(json);

if (event != null) {
_ref.read(transactionIntentProvider.notifier).state = event;
navigatorKey.currentState?.pushNamed('/transactions');
}
}

Expand All @@ -111,7 +111,12 @@ class TransactionService {
event = PendingTransactionEvent.fromJson(json);
}
} catch (e) {
print('Failed deserializing event: $e');
debugPrint('Failed deserializing $txType event: $e');
TelemetryService().sendError(
'Failed deserializing $txType event',
error: e.runtimeType.toString(),
stackTrace: StackTrace.current,
);
}

return event;
Expand Down
3 changes: 0 additions & 3 deletions mobile-app/lib/shared/global_navigator_key.dart

This file was deleted.

66 changes: 48 additions & 18 deletions mobile-app/lib/v2/screens/home/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:resonance_network_wallet/v2/components/quantus_button.dart';
import 'package:resonance_network_wallet/v2/components/quantus_icon_button.dart';
import 'package:resonance_network_wallet/v2/components/scaffold_base_bottom_content.dart';
import 'package:resonance_network_wallet/v2/screens/accounts/open_accounts_management_button.dart';
import 'package:resonance_network_wallet/v2/screens/activity/transaction_detail_sheet.dart';
import 'package:resonance_network_wallet/v2/screens/receive/receive_screen.dart';
import 'package:resonance_network_wallet/v2/screens/send/input_amount_screen.dart';
import 'package:resonance_network_wallet/v2/screens/send/select_recipient_screen.dart';
Expand Down Expand Up @@ -40,6 +41,53 @@ class HomeScreen extends ConsumerStatefulWidget {
}

class _HomeScreenState extends ConsumerState<HomeScreen> {
@override
void initState() {
super.initState();

ref.listenManual<TransactionEvent?>(transactionIntentProvider, _onTransactionIntent);
ref.listenManual<PaymentIntent?>(paymentIntentProvider, _onPaymentIntent);
ref.listenManual<String?>(sharedAccountIntentProvider, _onSharedIntent);
ref.listenManual<AsyncValue<DisplayAccount?>>(activeAccountProvider, (_, async) {
if (async.value == null) return;
_onTransactionIntent(null, ref.read(transactionIntentProvider));
});

Future.microtask(_drainPendingIntents);
}

void _drainPendingIntents() {
if (!mounted) return;
_onTransactionIntent(null, ref.read(transactionIntentProvider));
_onPaymentIntent(null, ref.read(paymentIntentProvider));
_onSharedIntent(null, ref.read(sharedAccountIntentProvider));
}

void _onTransactionIntent(TransactionEvent? _, TransactionEvent? transaction) {
if (transaction == null || !mounted) return;
final active = ref.read(activeAccountProvider).value;
if (active == null) return;
ref.read(transactionIntentProvider.notifier).state = null;
showTransactionDetailSheet(context, transaction, active.account.accountId);
}

void _onPaymentIntent(PaymentIntent? _, PaymentIntent? payment) {
if (payment == null || !mounted) return;
ref.read(paymentIntentProvider.notifier).state = null;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => InputAmountScreen(recipientAddress: payment.to, initialAmount: payment.amount, isPayMode: true),
),
);
}

void _onSharedIntent(String? _, String? shared) {
if (shared == null || !mounted) return;
ref.read(sharedAccountIntentProvider.notifier).state = null;
showSharedAddressActionSheet(context, shared);
}

Future<void> _refresh() async {
final active = ref.read(activeAccountProvider).value;
ref.invalidate(balanceProviderFamily);
Expand Down Expand Up @@ -70,24 +118,6 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {

@override
Widget build(BuildContext context) {
ref.listen(paymentIntentProvider, (_, payment) {
if (payment == null) return;
ref.read(paymentIntentProvider.notifier).state = null;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
InputAmountScreen(recipientAddress: payment.to, initialAmount: payment.amount, isPayMode: true),
),
);
});

ref.listen(sharedAccountIntentProvider, (_, shared) {
if (shared == null) return;
ref.read(sharedAccountIntentProvider.notifier).state = null;
showSharedAddressActionSheet(context, shared);
});

final accountAsync = ref.watch(activeAccountProvider);
final txAsync = ref.watch(activeAccountTransactionsProvider(TransactionFilter.all));
final colors = context.colors;
Expand Down
Loading
Loading