From 87e1c91801faa0cf873a6fa374d06b031787a3db Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 17 Feb 2026 17:54:08 +0800 Subject: [PATCH 01/19] feat: update adapter implementation details for remote message --- .../services/firebase_messaging_service.dart | 53 +++++-------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/mobile-app/lib/services/firebase_messaging_service.dart b/mobile-app/lib/services/firebase_messaging_service.dart index dd5682e55..e04bc67f4 100644 --- a/mobile-app/lib/services/firebase_messaging_service.dart +++ b/mobile-app/lib/services/firebase_messaging_service.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/models/notification_models.dart'; +import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/providers/notification_provider.dart'; import 'package:resonance_network_wallet/services/transaction_service.dart'; @@ -141,47 +142,21 @@ class FirebaseMessagingService { /// Convert an FCM [RemoteMessage] into the app's [NotificationData] model. NotificationData? _remoteMessageToNotificationData(RemoteMessage message) { - final notification = message.notification; final data = message.data; - final title = notification?.title ?? data['title'] as String? ?? 'Notification'; - final body = notification?.body ?? data['body'] as String? ?? ''; - - // Parse optional fields from the data payload. - final accountId = data['accountId'] as String? ?? ''; - final accountName = data['accountName'] as String? ?? ''; - final typeStr = data['type'] as String?; - final intentStr = data['intent'] as String?; - - final type = NotificationType.values.firstWhere((e) => e.name == typeStr, orElse: () => NotificationType.info); - - final intent = NotificationIntent.values.firstWhere( - (e) => e.name == intentStr, - orElse: () => NotificationIntent.others, - ); - - // Build metadata from the data payload (excluding fields we already extracted). - final metadata = Map.from(data) - ..remove('title') - ..remove('body') - ..remove('accountId') - ..remove('accountName') - ..remove('type') - ..remove('intent'); - - return NotificationData( - id: 'remote_${message.messageId ?? DateTime.now().millisecondsSinceEpoch}', - accountId: accountId, - type: type, - intent: intent, - source: NotificationSource.remote, - title: title, - message: body, - accountName: accountName, - timestamp: DateTime.now(), - persistent: true, - metadata: metadata.isNotEmpty ? metadata : null, - ); + final txService = _ref.read(transactionServiceProvider); + final event = txService.deserializeTxEventFromJsonIfPossible(data); + if (event == null) return null; + + if (event is TransferEvent) { + final account = _ref.read(accountsProvider.notifier).getAccountWithId(event.to); + return NotificationTemplates.tokenReceived(account: account, transactionData: event); + } else if (event is ReversibleTransferEvent) { + final account = _ref.read(accountsProvider.notifier).getAccountWithId(event.to); + return NotificationTemplates.reversibleTransactionReminder(account: account, transactionData: event); + } + + return null; } } From 302316e92509136a3c6f99121bebee7574adbd72 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 17 Feb 2026 20:34:14 +0800 Subject: [PATCH 02/19] feat: add real endpoint --- quantus_sdk/lib/src/constants/app_constants.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quantus_sdk/lib/src/constants/app_constants.dart b/quantus_sdk/lib/src/constants/app_constants.dart index 131d070e8..11d8994b3 100644 --- a/quantus_sdk/lib/src/constants/app_constants.dart +++ b/quantus_sdk/lib/src/constants/app_constants.dart @@ -23,7 +23,7 @@ class AppConstants { // static const String taskMasterEndpoint = 'http://localhost:3000/api'; static const String taskMasterEndpoint = 'https://quests.quantus.com/api'; - static const String senotiEndpoint = 'http://localhost:3100/api'; + static const String senotiEndpoint = 'https://snt.quantus.com/api'; static const String explorerEndpoint = 'https://explorer.quantus.com'; static const String helpAndSupportUrl = 'https://t.me/c/quantusnetwork/2457'; From 8616d7410f5f783cba95a2503405b30419e092e8 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 12:41:15 +0800 Subject: [PATCH 03/19] feat: update senoti and fcm service to support multiple addresses and also unregister + insert new --- .../services/firebase_messaging_service.dart | 77 ++++++--- .../lib/src/services/senoti_service.dart | 146 +++++++++++++++--- 2 files changed, 180 insertions(+), 43 deletions(-) diff --git a/mobile-app/lib/services/firebase_messaging_service.dart b/mobile-app/lib/services/firebase_messaging_service.dart index e04bc67f4..3e2f27e4f 100644 --- a/mobile-app/lib/services/firebase_messaging_service.dart +++ b/mobile-app/lib/services/firebase_messaging_service.dart @@ -13,8 +13,6 @@ import 'package:resonance_network_wallet/services/transaction_service.dart'; /// Must be a top-level function (not a class method) for Firebase. @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { - // Background messages are automatically shown by the OS as notifications. - // No additional handling is needed here unless you want to persist data. debugPrint('FCM background message: ${message.messageId}'); } @@ -24,9 +22,19 @@ class FirebaseMessagingService { final SenotiService _senotiService = SenotiService(); bool _isInitialized = false; + String? _cachedToken; FirebaseMessagingService(this._ref); + String get _platform => Platform.operatingSystem; + + /// Returns the cached FCM device token, fetching from Firebase if not yet available. + Future getDeviceToken() async { + if (_cachedToken != null) return _cachedToken; + _cachedToken = await _messaging.getToken(); + return _cachedToken; + } + /// Initialize FCM: request permissions, get token, and set up listeners. Future init() async { if (_isInitialized) return; @@ -37,7 +45,7 @@ class FirebaseMessagingService { return; } - await _getToken(); + await _fetchAndRegisterToken(); _setupForegroundMessageListener(); _setupTokenRefreshListener(); @@ -52,9 +60,6 @@ class FirebaseMessagingService { debugPrint('FCM permission status: ${settings.authorizationStatus}'); - // On iOS, set foreground notification presentation options. - // This tells iOS to NOT show the system banner when the app is in the - // foreground, because we handle it ourselves via local notifications. if (Platform.isIOS) { await _messaging.setForegroundNotificationPresentationOptions(alert: false, badge: true, sound: false); } @@ -62,33 +67,63 @@ class FirebaseMessagingService { return settings.authorizationStatus; } - Future _registerDevice(String token) async { - try { - await _senotiService.registerDevice(token, Platform.operatingSystem); - } catch (e) { - debugPrint('Failed to register device: $e'); - } - } - - /// Get the FCM device token (useful for server-side targeting). - Future _getToken() async { + Future _fetchAndRegisterToken() async { final token = await _messaging.getToken(); debugPrint('FCM token: $token'); if (token != null && token.isNotEmpty) { + _cachedToken = token; await _registerDevice(token); } } - /// Listen for token refresh events. + Future _registerDevice(String token) async { + try { + await _senotiService.registerDevice(token, _platform); + } catch (e) { + debugPrint('Failed to register device: $e'); + rethrow; + } + } + void _setupTokenRefreshListener() { _messaging.onTokenRefresh.listen((newToken) async { debugPrint('FCM token refreshed: $newToken'); - + _cachedToken = newToken; await _registerDevice(newToken); }); } + /// Unregister this device from push notifications (e.g. on wallet reset/logout). + Future unregisterDevice() async { + final token = await getDeviceToken(); + if (token == null) { + debugPrint('No FCM token available — skipping unregister'); + return; + } + try { + await _senotiService.unregisterDevice(token, _platform); + } catch (e) { + debugPrint('Failed to unregister device: $e'); + rethrow; + } + } + + /// Register a newly created address for push notifications on this device. + Future insertNewAddress(String newAddress) async { + final token = await getDeviceToken(); + if (token == null) { + debugPrint('No FCM token available — skipping insertNewAddress'); + return; + } + try { + await _senotiService.insertNewAddress(newAddress: newAddress, deviceToken: token, platform: _platform); + } catch (e) { + debugPrint('Failed to insert new address: $e'); + rethrow; + } + } + /// Listen for messages when the app is in the foreground. /// FCM does NOT show a system notification in this case, so we convert /// the message to a NotificationData and show it via local notifications. @@ -99,7 +134,6 @@ class FirebaseMessagingService { final notification = _remoteMessageToNotificationData(message); if (notification == null) return; - // Add to the notification provider (persists + sends to stream). final notifier = _ref.read(notificationProvider.notifier); notifier.addNotification(notification); }); @@ -112,17 +146,14 @@ 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 navigatorKey) { - // Handle tap when app was in background (not terminated). FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) { debugPrint('FCM notification tapped (background): ${message.messageId}'); _handleNotificationTap(message, navigatorKey); }); - // Handle tap when app was terminated. _handleInitialMessage(navigatorKey); } - /// Check if the app was launched from a terminated state by tapping an FCM notification. Future _handleInitialMessage(GlobalKey navigatorKey) async { final initialMessage = await _messaging.getInitialMessage(); if (initialMessage != null) { @@ -131,7 +162,6 @@ class FirebaseMessagingService { } } - /// Navigate based on the FCM message data payload. void _handleNotificationTap(RemoteMessage message, GlobalKey navigatorKey) { final data = message.data; if (data.isEmpty) return; @@ -140,7 +170,6 @@ class FirebaseMessagingService { txService.navigateToTransactionFromPayloadIfPossible(data, navigatorKey); } - /// Convert an FCM [RemoteMessage] into the app's [NotificationData] model. NotificationData? _remoteMessageToNotificationData(RemoteMessage message) { final data = message.data; diff --git a/quantus_sdk/lib/src/services/senoti_service.dart b/quantus_sdk/lib/src/services/senoti_service.dart index dc8ffe179..5f0c1525c 100644 --- a/quantus_sdk/lib/src/services/senoti_service.dart +++ b/quantus_sdk/lib/src/services/senoti_service.dart @@ -23,34 +23,102 @@ class SenotiAuthClient { return {'temp_session_id': j['temp_session_id'] as String, 'challenge': j['challenge'] as String}; } - Future registerDevice({ + Future> _buildAuthHeaders({ required String ss58Address, required String publicKeyHex, required Future Function(List messageBytes) signHex, - required String token, + required String deviceToken, required String platform, }) async { final ch = await requestChallenge(); final msg = - 'device-registrar:device-registration:1|challenge=${ch['challenge']}|address=$ss58Address|platform=$platform|token=$token'; + 'device-registrar:authentication:1|challenge=${ch['challenge']}|address=$ss58Address|platform=$platform|device_token=$deviceToken'; final sigHex = await signHex(utf8.encode(msg)); + return { + 'content-type': 'application/json', + 'x-public-key': publicKeyHex, + 'x-sign-address': ss58Address, + 'x-signature': sigHex, + 'x-platform': platform, + 'x-temp-session-id': ch['temp_session_id']!, + 'x-device-token': deviceToken, + }; + } + + Future registerDevice({ + required List addresses, + required String ss58Address, + required String publicKeyHex, + required Future Function(List messageBytes) signHex, + required String deviceToken, + required String platform, + }) async { + final headers = await _buildAuthHeaders( + ss58Address: ss58Address, + publicKeyHex: publicKeyHex, + signHex: signHex, + deviceToken: deviceToken, + platform: platform, + ); final r = await _client.post( Uri.parse('$senotiEndpointUrl/devices'), - headers: {'content-type': 'application/json'}, - body: jsonEncode({ - 'temp_session_id': ch['temp_session_id']!, - 'address': ss58Address, - 'public_key': publicKeyHex, - 'signature': sigHex, - }), + headers: headers, + body: jsonEncode({'addresses': addresses}), ); if (r.statusCode != 202) { - throw Exception('verify failed: ${r.statusCode}'); + throw Exception('register device failed: ${r.statusCode} ${r.body}'); + } + } + + Future unregisterDevice({ + required String ss58Address, + required String publicKeyHex, + required Future Function(List messageBytes) signHex, + required String deviceToken, + required String platform, + }) async { + final headers = await _buildAuthHeaders( + ss58Address: ss58Address, + publicKeyHex: publicKeyHex, + signHex: signHex, + deviceToken: deviceToken, + platform: platform, + ); + final r = await _client.delete( + Uri.parse('$senotiEndpointUrl/devices'), + headers: headers, + ); + if (r.statusCode != 202) { + throw Exception('unregister device failed: ${r.statusCode} ${r.body}'); + } + } + + Future insertNewAddress({ + required String newAddress, + required String ss58Address, + required String publicKeyHex, + required Future Function(List messageBytes) signHex, + required String deviceToken, + required String platform, + }) async { + final headers = await _buildAuthHeaders( + ss58Address: ss58Address, + publicKeyHex: publicKeyHex, + signHex: signHex, + deviceToken: deviceToken, + platform: platform, + ); + final r = await _client.post( + Uri.parse('$senotiEndpointUrl/devices/addresses'), + headers: headers, + body: jsonEncode({'address': newAddress}), + ); + if (r.statusCode != 202) { + throw Exception('insert new address failed: ${r.statusCode} ${r.body}'); } } } -// Senoti service singleton class SenotiService { static final SenotiService _instance = SenotiService._internal(); factory SenotiService() => _instance; @@ -61,25 +129,65 @@ class SenotiService { SenotiAuthClient get _client => SenotiAuthClient(AppConstants.senotiEndpoint); - Future registerDevice(String token, String platform) async { + Future<({String ss58Address, String publicKeyHex, Future Function(List) signHex})> + _getAccount1Credentials() async { final mnemonic = await _settingsService.getMnemonic(0); if (mnemonic == null) { throw Exception('Mnemonic not found.'); } final keypair = _hd.keyPairAtIndex(mnemonic, 0); - final ss58Address = keypair.ss58Address; - final publicKeyHex = convert_hex.hex.encode(keypair.publicKey); Future signHex(List messageBytes) async { final sig = crypto.signMessage(keypair: keypair, message: messageBytes); return convert_hex.hex.encode(sig); } - await _client.registerDevice( - ss58Address: ss58Address, - publicKeyHex: publicKeyHex, + return ( + ss58Address: keypair.ss58Address, + publicKeyHex: convert_hex.hex.encode(keypair.publicKey), signHex: signHex, - token: token, + ); + } + + Future registerDevice(String token, String platform) async { + final creds = await _getAccount1Credentials(); + final allAddresses = (await _settingsService.getAccounts()).map((a) => a.accountId).toList(); + + await _client.registerDevice( + addresses: allAddresses, + ss58Address: creds.ss58Address, + publicKeyHex: creds.publicKeyHex, + signHex: creds.signHex, + deviceToken: token, + platform: platform, + ); + } + + Future unregisterDevice(String token, String platform) async { + final creds = await _getAccount1Credentials(); + + await _client.unregisterDevice( + ss58Address: creds.ss58Address, + publicKeyHex: creds.publicKeyHex, + signHex: creds.signHex, + deviceToken: token, + platform: platform, + ); + } + + Future insertNewAddress({ + required String newAddress, + required String deviceToken, + required String platform, + }) async { + final creds = await _getAccount1Credentials(); + + await _client.insertNewAddress( + newAddress: newAddress, + ss58Address: creds.ss58Address, + publicKeyHex: creds.publicKeyHex, + signHex: creds.signHex, + deviceToken: deviceToken, platform: platform, ); } From f446c005c34c731bff8acae3ac8e295586d15aee Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 12:41:37 +0800 Subject: [PATCH 04/19] chore: pubspec.lock --- mobile-app/pubspec.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index f604a6d3f..101160ba7 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -869,10 +869,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" url: "https://pub.dev" source: hosted - version: "0.8.13+6" + version: "0.8.13+3" image_picker_linux: dependency: transitive description: @@ -1965,5 +1965,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.10.0 <4.0.0" - flutter: ">=3.38.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" From 3291f9a00a71e90994c7e300f3f522f61a78f02c Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 12:42:10 +0800 Subject: [PATCH 05/19] feat: add unregister device on reset --- mobile-app/lib/utils/feature_flags.dart | 2 +- .../lib/v2/screens/settings/settings_screen.dart | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/mobile-app/lib/utils/feature_flags.dart b/mobile-app/lib/utils/feature_flags.dart index 84849f17d..717143257 100644 --- a/mobile-app/lib/utils/feature_flags.dart +++ b/mobile-app/lib/utils/feature_flags.dart @@ -3,6 +3,6 @@ class FeatureFlags { static const bool enableTestButtons = false; // Only show in debug mode static const bool enableKeystoneHardwareWallet = false; // turn keystone hw wallet on and off static const bool enableHighSecurity = true; // turn keystone hw wallet on and off - static const bool enableRemoteNotifications = false; // turn remote notifications on and off + static const bool enableRemoteNotifications = true; // turn remote notifications on and off static const bool enableSwap = false; } diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index 24cf5dfb5..eca78aa90 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/reset_confirmation_bottom_sheet.dart'; +import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; +import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/select_wallet_screen.dart'; import 'package:resonance_network_wallet/v2/screens/welcome/welcome_screen.dart'; @@ -61,13 +63,24 @@ class _SettingsScreenV2State extends ConsumerState { }); } - void _resetAndClearData() { + Future _resetAndClearData() async { + try { + await ref.read(firebaseMessagingServiceProvider).unregisterDevice(); + } catch (e) { + if (mounted) { + context.showErrorToaster(message: 'Failed to unregister device: $e'); + } + + return; + } + _settingsService.clearAll(); SubstrateService().logout(); ref.read(pendingTransactionsProvider.notifier).clear(); ref.read(accountsProvider.notifier).reset(); ref.read(activeAccountProvider.notifier).reset(); ref.read(accountAssociationsProvider.notifier).reset(); + if (mounted) { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const WelcomeScreenV2()), (r) => false); } From 1424126cd6f310cb21b2cdb84e27244c9c7ef82c Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 13:45:59 +0800 Subject: [PATCH 06/19] feat: properly handle registering and addding new address to remote notifications --- .../services/firebase_messaging_service.dart | 36 ++++++++++--------- .../screens/create/wallet_ready_screen.dart | 2 ++ .../lib/v2/screens/home/accounts_sheet.dart | 3 ++ .../screens/import/import_wallet_screen.dart | 2 ++ .../lib/src/services/senoti_service.dart | 11 +++--- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/mobile-app/lib/services/firebase_messaging_service.dart b/mobile-app/lib/services/firebase_messaging_service.dart index 3e2f27e4f..930e5bfd1 100644 --- a/mobile-app/lib/services/firebase_messaging_service.dart +++ b/mobile-app/lib/services/firebase_messaging_service.dart @@ -30,8 +30,9 @@ class FirebaseMessagingService { /// Returns the cached FCM device token, fetching from Firebase if not yet available. Future getDeviceToken() async { - if (_cachedToken != null) return _cachedToken; - _cachedToken = await _messaging.getToken(); + _cachedToken ??= await _messaging.getToken(); + debugPrint('FCM token: $_cachedToken'); + return _cachedToken; } @@ -45,7 +46,8 @@ class FirebaseMessagingService { return; } - await _fetchAndRegisterToken(); + await getDeviceToken(); + await registerDeviceIfPossible(); _setupForegroundMessageListener(); _setupTokenRefreshListener(); @@ -67,30 +69,30 @@ class FirebaseMessagingService { return settings.authorizationStatus; } - Future _fetchAndRegisterToken() async { - final token = await _messaging.getToken(); - debugPrint('FCM token: $token'); - - if (token != null && token.isNotEmpty) { - _cachedToken = token; - await _registerDevice(token); - } - } - - Future _registerDevice(String token) async { + Future _tryRegisterDevice(String token) async { try { await _senotiService.registerDevice(token, _platform); } catch (e) { debugPrint('Failed to register device: $e'); - rethrow; } } + /// Register the device with the push notification backend. + /// Call this after the user creates or imports a wallet for the first time. + Future registerDeviceIfPossible() async { + final token = await getDeviceToken(); + if (token == null) { + debugPrint('No FCM token available — skipping device registration'); + return; + } + await _tryRegisterDevice(token); + } + void _setupTokenRefreshListener() { _messaging.onTokenRefresh.listen((newToken) async { debugPrint('FCM token refreshed: $newToken'); _cachedToken = newToken; - await _registerDevice(newToken); + await _tryRegisterDevice(newToken); }); } @@ -116,11 +118,11 @@ class FirebaseMessagingService { debugPrint('No FCM token available — skipping insertNewAddress'); return; } + try { await _senotiService.insertNewAddress(newAddress: newAddress, deviceToken: token, platform: _platform); } catch (e) { debugPrint('Failed to insert new address: $e'); - rethrow; } } diff --git a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart index 945df0ead..91ce280a8 100644 --- a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart +++ b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/main/screens/create_wallet_and_backup_screen.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; import 'package:resonance_network_wallet/services/referral_service.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; @@ -87,6 +88,7 @@ class _WalletReadyScreenV2State extends ConsumerState { } ref.invalidate(accountsProvider); ref.invalidate(activeAccountProvider); + await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); if (!mounted) return; Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const HomeScreen()), (route) => false); diff --git a/mobile-app/lib/v2/screens/home/accounts_sheet.dart b/mobile-app/lib/v2/screens/home/accounts_sheet.dart index 9b8de52ff..cb7fc1b9b 100644 --- a/mobile-app/lib/v2/screens/home/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/home/accounts_sheet.dart @@ -7,6 +7,7 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/account_gradient_image.dart'; import 'package:resonance_network_wallet/features/components/app_modal_bottom_sheet.dart'; import 'package:resonance_network_wallet/features/main/screens/add_hardware_account_screen.dart'; +import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; import 'package:resonance_network_wallet/shared/utils/share_utils.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; @@ -788,6 +789,8 @@ class _AccountsScreenState extends ConsumerState { await _accountsService.addAccount(accountToSave); ref.invalidate(accountsProvider); ref.invalidate(activeAccountProvider); + await ref.read(firebaseMessagingServiceProvider).insertNewAddress(accountToSave.accountId); + if (mounted) { _closeCreateView(); } diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart index b5bdf12e1..c1c10032b 100644 --- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; +import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; import 'package:resonance_network_wallet/v2/components/gradient_background.dart'; @@ -52,6 +53,7 @@ class _ImportWalletScreenV2State extends ConsumerState { await _discoverAccounts(mnemonic); _settingsService.setReferralCheckCompleted(); _settingsService.setExistingUserSeenPromoVideo(); + await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); if (!mounted) return; Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const HomeScreen()), (route) => false); diff --git a/quantus_sdk/lib/src/services/senoti_service.dart b/quantus_sdk/lib/src/services/senoti_service.dart index 5f0c1525c..83366d1db 100644 --- a/quantus_sdk/lib/src/services/senoti_service.dart +++ b/quantus_sdk/lib/src/services/senoti_service.dart @@ -84,10 +84,7 @@ class SenotiAuthClient { deviceToken: deviceToken, platform: platform, ); - final r = await _client.delete( - Uri.parse('$senotiEndpointUrl/devices'), - headers: headers, - ); + final r = await _client.delete(Uri.parse('$senotiEndpointUrl/devices'), headers: headers); if (r.statusCode != 202) { throw Exception('unregister device failed: ${r.statusCode} ${r.body}'); } @@ -130,7 +127,7 @@ class SenotiService { SenotiAuthClient get _client => SenotiAuthClient(AppConstants.senotiEndpoint); Future<({String ss58Address, String publicKeyHex, Future Function(List) signHex})> - _getAccount1Credentials() async { + _getAccount1Credentials() async { final mnemonic = await _settingsService.getMnemonic(0); if (mnemonic == null) { throw Exception('Mnemonic not found.'); @@ -150,8 +147,10 @@ class SenotiService { } Future registerDevice(String token, String platform) async { - final creds = await _getAccount1Credentials(); final allAddresses = (await _settingsService.getAccounts()).map((a) => a.accountId).toList(); + if (allAddresses.isEmpty) return; + + final creds = await _getAccount1Credentials(); await _client.registerDevice( addresses: allAddresses, From 4153b47796fa0a50d38e426c5b9c4dec3f633608 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 13:47:16 +0800 Subject: [PATCH 07/19] chore: formatting --- mobile-app/lib/services/firebase_messaging_service.dart | 2 +- mobile-app/lib/v2/screens/settings/settings_screen.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile-app/lib/services/firebase_messaging_service.dart b/mobile-app/lib/services/firebase_messaging_service.dart index 930e5bfd1..0b63f7c9f 100644 --- a/mobile-app/lib/services/firebase_messaging_service.dart +++ b/mobile-app/lib/services/firebase_messaging_service.dart @@ -118,7 +118,7 @@ class FirebaseMessagingService { debugPrint('No FCM token available — skipping insertNewAddress'); return; } - + try { await _senotiService.insertNewAddress(newAddress: newAddress, deviceToken: token, platform: _platform); } catch (e) { diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index eca78aa90..6f880b21d 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -80,7 +80,7 @@ class _SettingsScreenV2State extends ConsumerState { ref.read(accountsProvider.notifier).reset(); ref.read(activeAccountProvider.notifier).reset(); ref.read(accountAssociationsProvider.notifier).reset(); - + if (mounted) { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const WelcomeScreenV2()), (r) => false); } From 01206057910b7689bffc8c7926ca62607bb4027c Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 17:03:35 +0800 Subject: [PATCH 08/19] feat: add remote notification under feature flags --- .../v2/screens/create/wallet_ready_screen.dart | 6 +++++- .../v2/screens/import/import_wallet_screen.dart | 6 +++++- .../v2/screens/settings/settings_screen.dart | 17 ++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart index 91ce280a8..bc1c83c8e 100644 --- a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart +++ b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart @@ -7,6 +7,7 @@ import 'package:resonance_network_wallet/services/firebase_messaging_service.dar import 'package:resonance_network_wallet/services/referral_service.dart'; import 'package:resonance_network_wallet/shared/extensions/clipboard_extensions.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; +import 'package:resonance_network_wallet/utils/feature_flags.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; import 'package:resonance_network_wallet/v2/components/gradient_background.dart'; @@ -88,7 +89,10 @@ class _WalletReadyScreenV2State extends ConsumerState { } ref.invalidate(accountsProvider); ref.invalidate(activeAccountProvider); - await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); + + if (FeatureFlags.enableRemoteNotifications) { + await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); + } if (!mounted) return; Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const HomeScreen()), (route) => false); diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart index c1c10032b..d58301636 100644 --- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/providers/account_providers.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; +import 'package:resonance_network_wallet/utils/feature_flags.dart'; import 'package:resonance_network_wallet/v2/components/back_button.dart'; import 'package:resonance_network_wallet/v2/components/glass_container.dart'; import 'package:resonance_network_wallet/v2/components/gradient_background.dart'; @@ -53,7 +54,10 @@ class _ImportWalletScreenV2State extends ConsumerState { await _discoverAccounts(mnemonic); _settingsService.setReferralCheckCompleted(); _settingsService.setExistingUserSeenPromoVideo(); - await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); + + if (FeatureFlags.enableRemoteNotifications) { + await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); + } if (!mounted) return; Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const HomeScreen()), (route) => false); diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index 6f880b21d..fe555f443 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -5,6 +5,7 @@ import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/reset_confirmation_bottom_sheet.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; +import 'package:resonance_network_wallet/utils/feature_flags.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/select_wallet_screen.dart'; import 'package:resonance_network_wallet/v2/screens/welcome/welcome_screen.dart'; @@ -64,14 +65,16 @@ class _SettingsScreenV2State extends ConsumerState { } Future _resetAndClearData() async { - try { - await ref.read(firebaseMessagingServiceProvider).unregisterDevice(); - } catch (e) { - if (mounted) { - context.showErrorToaster(message: 'Failed to unregister device: $e'); - } + if (FeatureFlags.enableRemoteNotifications) { + try { + await ref.read(firebaseMessagingServiceProvider).unregisterDevice(); + } catch (e) { + if (mounted) { + context.showErrorToaster(message: 'Failed to unregister device: $e'); + } - return; + return; + } } _settingsService.clearAll(); From 3a7d27b116fb70af565cc7029b8442b71f982ae9 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 17:39:20 +0800 Subject: [PATCH 09/19] feat: finish updating reset confirm sheet --- .../settings/reset_confirmation_sheet.dart | 69 +++++++++++++++++++ .../v2/screens/settings/settings_screen.dart | 8 +-- 2 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 mobile-app/lib/v2/screens/settings/reset_confirmation_sheet.dart diff --git a/mobile-app/lib/v2/screens/settings/reset_confirmation_sheet.dart b/mobile-app/lib/v2/screens/settings/reset_confirmation_sheet.dart new file mode 100644 index 000000000..43830ba57 --- /dev/null +++ b/mobile-app/lib/v2/screens/settings/reset_confirmation_sheet.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:quantus_sdk/quantus_sdk.dart'; +import 'package:resonance_network_wallet/v2/components/bottom_sheet_container.dart'; +import 'package:resonance_network_wallet/v2/components/glass_container.dart'; +import 'package:resonance_network_wallet/v2/theme/app_colors.dart'; +import 'package:resonance_network_wallet/v2/theme/app_text_styles.dart'; + +class ResetConfirmationSheet extends StatefulWidget { + final VoidCallback onReset; + const ResetConfirmationSheet({super.key, required this.onReset}); + + @override + State createState() => _ResetConfirmationSheetState(); +} + +class _ResetConfirmationSheetState extends State { + bool _isCheckboxChecked = false; + + @override + Widget build(BuildContext context) { + final buttonTextStyle = context.themeText.paragraph?.copyWith(fontWeight: FontWeight.w500); + + return BottomSheetContainer( + title: 'Confirm Reset', + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 32), + Text( + 'Are you sure you want to proceed? This will delete all local wallet data. Make sure you have backed up your recovery phrase.', + style: context.themeText.smallParagraph, + ), + const SizedBox(height: 64), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + controlAffinity: ListTileControlAffinity.leading, + value: _isCheckboxChecked, + onChanged: (bool? value) { + setState(() { + _isCheckboxChecked = value ?? false; + }); + }, + activeColor: context.colors.success, + checkColor: context.colors.success, + side: const BorderSide(color: Colors.white), + title: Text('I have backed up my recovery phrase', style: context.themeText.smallParagraph), + ), + const SizedBox(height: 64), + GlassContainer( + asset: GlassContainer.wideAsset, + onTap: widget.onReset, + child: Center(child: Text('Confirm', style: buttonTextStyle)), + ), + const SizedBox(height: 16), + InkWell( + onTap: () => Navigator.pop(context), + child: Center( + child: Text('Cancel', style: buttonTextStyle?.copyWith(color: context.colors.textPrimary.useOpacity(0.5))), + ), + ), + ], + ), + ); + } +} + +void showResetConfirmationSheetV2(BuildContext context, VoidCallback onReset) { + BottomSheetContainer.show(context, builder: (_) => ResetConfirmationSheet(onReset: onReset)); +} diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index fe555f443..bb33bb662 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -2,11 +2,11 @@ import 'package:flutter/cupertino.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/features/components/reset_confirmation_bottom_sheet.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/utils/feature_flags.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_screen.dart'; +import 'package:resonance_network_wallet/v2/screens/settings/reset_confirmation_sheet.dart'; import 'package:resonance_network_wallet/v2/screens/settings/select_wallet_screen.dart'; import 'package:resonance_network_wallet/v2/screens/welcome/welcome_screen.dart'; import 'package:resonance_network_wallet/providers/account_associations_providers.dart'; @@ -90,11 +90,7 @@ class _SettingsScreenV2State extends ConsumerState { } void _showResetConfirmation() { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (_) => ResetConfirmationBottomSheet(onReset: _resetAndClearData), - ); + showResetConfirmationSheetV2(context, _resetAndClearData); } String _timeLimitLabel() { From 6e7d0ce790b33b981e4385029d80c303ca3b2bf0 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 17:41:49 +0800 Subject: [PATCH 10/19] feat: disable on not checked confirmation --- mobile-app/lib/v2/screens/import/import_wallet_screen.dart | 2 +- .../lib/v2/screens/settings/reset_confirmation_sheet.dart | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart index d58301636..48ccd9606 100644 --- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart @@ -54,7 +54,7 @@ class _ImportWalletScreenV2State extends ConsumerState { await _discoverAccounts(mnemonic); _settingsService.setReferralCheckCompleted(); _settingsService.setExistingUserSeenPromoVideo(); - + if (FeatureFlags.enableRemoteNotifications) { await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); } diff --git a/mobile-app/lib/v2/screens/settings/reset_confirmation_sheet.dart b/mobile-app/lib/v2/screens/settings/reset_confirmation_sheet.dart index 43830ba57..2ba8f71ce 100644 --- a/mobile-app/lib/v2/screens/settings/reset_confirmation_sheet.dart +++ b/mobile-app/lib/v2/screens/settings/reset_confirmation_sheet.dart @@ -48,14 +48,17 @@ class _ResetConfirmationSheetState extends State { const SizedBox(height: 64), GlassContainer( asset: GlassContainer.wideAsset, - onTap: widget.onReset, + onTap: _isCheckboxChecked ? widget.onReset : null, child: Center(child: Text('Confirm', style: buttonTextStyle)), ), const SizedBox(height: 16), InkWell( onTap: () => Navigator.pop(context), child: Center( - child: Text('Cancel', style: buttonTextStyle?.copyWith(color: context.colors.textPrimary.useOpacity(0.5))), + child: Text( + 'Cancel', + style: buttonTextStyle?.copyWith(color: context.colors.textPrimary.useOpacity(0.5)), + ), ), ), ], From 5a19d98d268b202e4c6e551ceb1bfc08d1006622 Mon Sep 17 00:00:00 2001 From: Beast Date: Mon, 23 Feb 2026 17:51:20 +0800 Subject: [PATCH 11/19] chore: code formatting --- mobile-app/lib/v2/screens/import/import_wallet_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart index d58301636..48ccd9606 100644 --- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart @@ -54,7 +54,7 @@ class _ImportWalletScreenV2State extends ConsumerState { await _discoverAccounts(mnemonic); _settingsService.setReferralCheckCompleted(); _settingsService.setExistingUserSeenPromoVideo(); - + if (FeatureFlags.enableRemoteNotifications) { await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); } From 46884b971d1b56bf819be0421f850b7205dc2bd8 Mon Sep 17 00:00:00 2001 From: Beast Date: Tue, 24 Feb 2026 15:51:35 +0800 Subject: [PATCH 12/19] feat: remove auth for registering device remote notifications --- .../services/firebase_messaging_service.dart | 2 +- .../lib/src/services/senoti_service.dart | 154 +++--------------- 2 files changed, 20 insertions(+), 136 deletions(-) diff --git a/mobile-app/lib/services/firebase_messaging_service.dart b/mobile-app/lib/services/firebase_messaging_service.dart index 0b63f7c9f..d1116f894 100644 --- a/mobile-app/lib/services/firebase_messaging_service.dart +++ b/mobile-app/lib/services/firebase_messaging_service.dart @@ -120,7 +120,7 @@ class FirebaseMessagingService { } try { - await _senotiService.insertNewAddress(newAddress: newAddress, deviceToken: token, platform: _platform); + await _senotiService.insertNewAddress(newAddress: newAddress, deviceToken: token); } catch (e) { debugPrint('Failed to insert new address: $e'); } diff --git a/quantus_sdk/lib/src/services/senoti_service.dart b/quantus_sdk/lib/src/services/senoti_service.dart index 83366d1db..4342855c8 100644 --- a/quantus_sdk/lib/src/services/senoti_service.dart +++ b/quantus_sdk/lib/src/services/senoti_service.dart @@ -1,114 +1,49 @@ import 'dart:convert'; -import 'package:convert/convert.dart' as convert_hex; import 'package:http/http.dart' as http; import 'package:quantus_sdk/quantus_sdk.dart'; -import 'package:quantus_sdk/src/rust/api/crypto.dart' as crypto; class SenotiAuthClient { final String senotiEndpointUrl; final http.Client _client; - SenotiAuthClient(this.senotiEndpointUrl, {http.Client? client}) : _client = client ?? http.Client(); - - Future> requestChallenge() async { - final r = await _client.get( - Uri.parse('$senotiEndpointUrl/auth/request-challenge'), - headers: {'content-type': 'application/json'}, - ); - if (r.statusCode != 200) { - throw Exception('request-challenge failed: ${r.statusCode} ${r.body}'); - } - final j = jsonDecode(r.body) as Map; - return {'temp_session_id': j['temp_session_id'] as String, 'challenge': j['challenge'] as String}; + Map getAuthHeaders() { + return {'content-type': 'application/json'}; } - Future> _buildAuthHeaders({ - required String ss58Address, - required String publicKeyHex, - required Future Function(List messageBytes) signHex, - required String deviceToken, - required String platform, - }) async { - final ch = await requestChallenge(); - final msg = - 'device-registrar:authentication:1|challenge=${ch['challenge']}|address=$ss58Address|platform=$platform|device_token=$deviceToken'; - final sigHex = await signHex(utf8.encode(msg)); - return { - 'content-type': 'application/json', - 'x-public-key': publicKeyHex, - 'x-sign-address': ss58Address, - 'x-signature': sigHex, - 'x-platform': platform, - 'x-temp-session-id': ch['temp_session_id']!, - 'x-device-token': deviceToken, - }; - } + SenotiAuthClient(this.senotiEndpointUrl, {http.Client? client}) : _client = client ?? http.Client(); Future registerDevice({ required List addresses, - required String ss58Address, - required String publicKeyHex, - required Future Function(List messageBytes) signHex, required String deviceToken, required String platform, }) async { - final headers = await _buildAuthHeaders( - ss58Address: ss58Address, - publicKeyHex: publicKeyHex, - signHex: signHex, - deviceToken: deviceToken, - platform: platform, - ); final r = await _client.post( Uri.parse('$senotiEndpointUrl/devices'), - headers: headers, - body: jsonEncode({'addresses': addresses}), + headers: getAuthHeaders(), + body: jsonEncode({'addresses': addresses, 'device_token': deviceToken, 'platform': platform}), ); if (r.statusCode != 202) { throw Exception('register device failed: ${r.statusCode} ${r.body}'); } } - Future unregisterDevice({ - required String ss58Address, - required String publicKeyHex, - required Future Function(List messageBytes) signHex, - required String deviceToken, - required String platform, - }) async { - final headers = await _buildAuthHeaders( - ss58Address: ss58Address, - publicKeyHex: publicKeyHex, - signHex: signHex, - deviceToken: deviceToken, - platform: platform, + Future unregisterDevice({required String deviceToken}) async { + final r = await _client.delete( + Uri.parse('$senotiEndpointUrl/devices'), + headers: getAuthHeaders(), + body: jsonEncode({'device_token': deviceToken}), ); - final r = await _client.delete(Uri.parse('$senotiEndpointUrl/devices'), headers: headers); if (r.statusCode != 202) { throw Exception('unregister device failed: ${r.statusCode} ${r.body}'); } } - Future insertNewAddress({ - required String newAddress, - required String ss58Address, - required String publicKeyHex, - required Future Function(List messageBytes) signHex, - required String deviceToken, - required String platform, - }) async { - final headers = await _buildAuthHeaders( - ss58Address: ss58Address, - publicKeyHex: publicKeyHex, - signHex: signHex, - deviceToken: deviceToken, - platform: platform, - ); + Future insertNewAddress({required String newAddress, required String deviceToken}) async { final r = await _client.post( Uri.parse('$senotiEndpointUrl/devices/addresses'), - headers: headers, - body: jsonEncode({'address': newAddress}), + headers: getAuthHeaders(), + body: jsonEncode({'address': newAddress, 'device_token': deviceToken}), ); if (r.statusCode != 202) { throw Exception('insert new address failed: ${r.statusCode} ${r.body}'); @@ -121,73 +56,22 @@ class SenotiService { factory SenotiService() => _instance; SenotiService._internal(); - final SettingsService _settingsService = SettingsService(); - final HdWalletService _hd = HdWalletService(); + final AccountsService _accountsService = AccountsService(); SenotiAuthClient get _client => SenotiAuthClient(AppConstants.senotiEndpoint); - Future<({String ss58Address, String publicKeyHex, Future Function(List) signHex})> - _getAccount1Credentials() async { - final mnemonic = await _settingsService.getMnemonic(0); - if (mnemonic == null) { - throw Exception('Mnemonic not found.'); - } - final keypair = _hd.keyPairAtIndex(mnemonic, 0); - - Future signHex(List messageBytes) async { - final sig = crypto.signMessage(keypair: keypair, message: messageBytes); - return convert_hex.hex.encode(sig); - } - - return ( - ss58Address: keypair.ss58Address, - publicKeyHex: convert_hex.hex.encode(keypair.publicKey), - signHex: signHex, - ); - } - Future registerDevice(String token, String platform) async { - final allAddresses = (await _settingsService.getAccounts()).map((a) => a.accountId).toList(); + final allAddresses = (await _accountsService.getAccounts()).map((a) => a.accountId).toList(); if (allAddresses.isEmpty) return; - final creds = await _getAccount1Credentials(); - - await _client.registerDevice( - addresses: allAddresses, - ss58Address: creds.ss58Address, - publicKeyHex: creds.publicKeyHex, - signHex: creds.signHex, - deviceToken: token, - platform: platform, - ); + await _client.registerDevice(addresses: allAddresses, deviceToken: token, platform: platform); } Future unregisterDevice(String token, String platform) async { - final creds = await _getAccount1Credentials(); - - await _client.unregisterDevice( - ss58Address: creds.ss58Address, - publicKeyHex: creds.publicKeyHex, - signHex: creds.signHex, - deviceToken: token, - platform: platform, - ); + await _client.unregisterDevice(deviceToken: token); } - Future insertNewAddress({ - required String newAddress, - required String deviceToken, - required String platform, - }) async { - final creds = await _getAccount1Credentials(); - - await _client.insertNewAddress( - newAddress: newAddress, - ss58Address: creds.ss58Address, - publicKeyHex: creds.publicKeyHex, - signHex: creds.signHex, - deviceToken: deviceToken, - platform: platform, - ); + Future insertNewAddress({required String newAddress, required String deviceToken}) async { + await _client.insertNewAddress(newAddress: newAddress, deviceToken: deviceToken); } } From b119864372af19733c2115c55f7a038017db96ef Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 25 Feb 2026 00:06:06 +0800 Subject: [PATCH 13/19] chore: revert sdks but new sdks update some packages version --- mobile-app/pubspec.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/mobile-app/pubspec.lock b/mobile-app/pubspec.lock index 101160ba7..e19f20002 100644 --- a/mobile-app/pubspec.lock +++ b/mobile-app/pubspec.lock @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -869,10 +869,10 @@ packages: dependency: transitive description: name: image_picker_ios - sha256: "956c16a42c0c708f914021666ffcd8265dde36e673c9fa68c81f7d085d9774ad" + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 url: "https://pub.dev" source: hosted - version: "0.8.13+3" + version: "0.8.13+6" image_picker_linux: dependency: transitive description: @@ -1037,18 +1037,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" merlin: dependency: transitive description: @@ -1672,10 +1672,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" timezone: dependency: "direct main" description: @@ -1965,5 +1965,5 @@ packages: source: hosted version: "2.1.0" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.38.0" From fccdfe8f4e89230a440dc1728ddcc41c40754ab3 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 25 Feb 2026 00:06:28 +0800 Subject: [PATCH 14/19] chore: latest flutter somehow add this automatically --- mobile-app/ios/Flutter/AppFrameworkInfo.plist | 2 -- 1 file changed, 2 deletions(-) diff --git a/mobile-app/ios/Flutter/AppFrameworkInfo.plist b/mobile-app/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf765..391a902b2 100644 --- a/mobile-app/ios/Flutter/AppFrameworkInfo.plist +++ b/mobile-app/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 From dcae7d90fe2c09d42fec0a9c02229413f1f827ed Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 25 Feb 2026 00:19:25 +0800 Subject: [PATCH 15/19] feat: migrate to UIScene --- mobile-app/ios/Runner/AppDelegate.swift | 8 +- mobile-app/ios/Runner/Info.plist | 147 ++++++++++++++---------- 2 files changed, 89 insertions(+), 66 deletions(-) diff --git a/mobile-app/ios/Runner/AppDelegate.swift b/mobile-app/ios/Runner/AppDelegate.swift index 36cafd20b..8704619f4 100644 --- a/mobile-app/ios/Runner/AppDelegate.swift +++ b/mobile-app/ios/Runner/AppDelegate.swift @@ -4,7 +4,7 @@ import flutter_local_notifications import FirebaseMessaging @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -19,8 +19,6 @@ import FirebaseMessaging UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate } - GeneratedPluginRegistrant.register(with: self) - // Register for remote notifications (required when swizzling is disabled). application.registerForRemoteNotifications() @@ -36,4 +34,8 @@ import FirebaseMessaging Messaging.messaging().apnsToken = deviceToken super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/mobile-app/ios/Runner/Info.plist b/mobile-app/ios/Runner/Info.plist index 342faac78..ed888d871 100644 --- a/mobile-app/ios/Runner/Info.plist +++ b/mobile-app/ios/Runner/Info.plist @@ -1,70 +1,91 @@ + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Quantus + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Quantus + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + FirebaseAppDelegateProxyEnabled + + FlutterDeepLinkingEnabled + + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + https + + LSRequiresIPhoneOS + + NSCameraUsageDescription + We need camera access to scan QR codes for sending funds. + NSFaceIDUsageDescription + Use Face ID to authenticate and securely access your wallet. + UIApplicationSceneManifest - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Quantus - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Quantus - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - FlutterDeepLinkingEnabled - - ITSAppUsesNonExemptEncryption - - LSApplicationQueriesSchemes - - https - - LSRequiresIPhoneOS - - NSCameraUsageDescription - We need camera access to scan QR codes for sending funds. - NSFaceIDUsageDescription - Use Face ID to authenticate and securely access your wallet. - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - remote-notification - fetch - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - FirebaseAppDelegateProxyEnabled + UIApplicationSupportsMultipleScenes + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + remote-notification + fetch + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + From 70cb86bb297ef62fd6e4c22c7606e5b02d41bae8 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 25 Feb 2026 00:30:15 +0800 Subject: [PATCH 16/19] fix: blocking request register device, crashing on reset --- mobile-app/lib/v2/screens/create/wallet_ready_screen.dart | 2 +- mobile-app/lib/v2/screens/settings/settings_screen.dart | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart index bc1c83c8e..6e3fac6d5 100644 --- a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart +++ b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart @@ -91,7 +91,7 @@ class _WalletReadyScreenV2State extends ConsumerState { ref.invalidate(activeAccountProvider); if (FeatureFlags.enableRemoteNotifications) { - await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); + Future.microtask(() => ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible()); } if (!mounted) return; diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index fe555f443..f521b9534 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:quantus_sdk/quantus_sdk.dart'; import 'package:resonance_network_wallet/features/components/reset_confirmation_bottom_sheet.dart'; import 'package:resonance_network_wallet/services/firebase_messaging_service.dart'; -import 'package:resonance_network_wallet/shared/extensions/toaster_extensions.dart'; import 'package:resonance_network_wallet/utils/feature_flags.dart'; import 'package:resonance_network_wallet/v2/screens/settings/recovery_phrase_screen.dart'; import 'package:resonance_network_wallet/v2/screens/settings/select_wallet_screen.dart'; @@ -69,11 +68,7 @@ class _SettingsScreenV2State extends ConsumerState { try { await ref.read(firebaseMessagingServiceProvider).unregisterDevice(); } catch (e) { - if (mounted) { - context.showErrorToaster(message: 'Failed to unregister device: $e'); - } - - return; + debugPrint('Failed to unregister device (non-fatal): $e'); } } From 26de2dcb6ecc5283c73860559d0d3b058f7bc488 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 25 Feb 2026 14:45:16 +0800 Subject: [PATCH 17/19] feat: make all device token methods non blocking --- mobile-app/lib/v2/screens/create/wallet_ready_screen.dart | 2 +- mobile-app/lib/v2/screens/import/import_wallet_screen.dart | 2 +- mobile-app/lib/v2/screens/settings/settings_screen.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart index 6e3fac6d5..ca27b9187 100644 --- a/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart +++ b/mobile-app/lib/v2/screens/create/wallet_ready_screen.dart @@ -91,7 +91,7 @@ class _WalletReadyScreenV2State extends ConsumerState { ref.invalidate(activeAccountProvider); if (FeatureFlags.enableRemoteNotifications) { - Future.microtask(() => ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible()); + ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); } if (!mounted) return; diff --git a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart index 48ccd9606..d19fac1e5 100644 --- a/mobile-app/lib/v2/screens/import/import_wallet_screen.dart +++ b/mobile-app/lib/v2/screens/import/import_wallet_screen.dart @@ -56,7 +56,7 @@ class _ImportWalletScreenV2State extends ConsumerState { _settingsService.setExistingUserSeenPromoVideo(); if (FeatureFlags.enableRemoteNotifications) { - await ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); + ref.read(firebaseMessagingServiceProvider).registerDeviceIfPossible(); } if (!mounted) return; diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index f521b9534..b0bc151a5 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -66,7 +66,7 @@ class _SettingsScreenV2State extends ConsumerState { Future _resetAndClearData() async { if (FeatureFlags.enableRemoteNotifications) { try { - await ref.read(firebaseMessagingServiceProvider).unregisterDevice(); + ref.read(firebaseMessagingServiceProvider).unregisterDevice(); } catch (e) { debugPrint('Failed to unregister device (non-fatal): $e'); } From 5a145f83276c0e0879eecb5576cef1fbe36cc4d0 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 25 Feb 2026 14:45:59 +0800 Subject: [PATCH 18/19] leftover previous commit --- mobile-app/lib/v2/screens/home/accounts_sheet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile-app/lib/v2/screens/home/accounts_sheet.dart b/mobile-app/lib/v2/screens/home/accounts_sheet.dart index cb7fc1b9b..6b52d6b15 100644 --- a/mobile-app/lib/v2/screens/home/accounts_sheet.dart +++ b/mobile-app/lib/v2/screens/home/accounts_sheet.dart @@ -789,7 +789,7 @@ class _AccountsScreenState extends ConsumerState { await _accountsService.addAccount(accountToSave); ref.invalidate(accountsProvider); ref.invalidate(activeAccountProvider); - await ref.read(firebaseMessagingServiceProvider).insertNewAddress(accountToSave.accountId); + ref.read(firebaseMessagingServiceProvider).insertNewAddress(accountToSave.accountId); if (mounted) { _closeCreateView(); From 3cca25e7b0d2b228621528729585c6b569e873d6 Mon Sep 17 00:00:00 2001 From: Beast Date: Wed, 25 Feb 2026 15:49:16 +0800 Subject: [PATCH 19/19] feat: remove rethrow for unregister device --- mobile-app/lib/services/firebase_messaging_service.dart | 1 - mobile-app/lib/v2/screens/settings/settings_screen.dart | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/mobile-app/lib/services/firebase_messaging_service.dart b/mobile-app/lib/services/firebase_messaging_service.dart index d1116f894..82cceeef9 100644 --- a/mobile-app/lib/services/firebase_messaging_service.dart +++ b/mobile-app/lib/services/firebase_messaging_service.dart @@ -107,7 +107,6 @@ class FirebaseMessagingService { await _senotiService.unregisterDevice(token, _platform); } catch (e) { debugPrint('Failed to unregister device: $e'); - rethrow; } } diff --git a/mobile-app/lib/v2/screens/settings/settings_screen.dart b/mobile-app/lib/v2/screens/settings/settings_screen.dart index b0bc151a5..986bdb247 100644 --- a/mobile-app/lib/v2/screens/settings/settings_screen.dart +++ b/mobile-app/lib/v2/screens/settings/settings_screen.dart @@ -65,11 +65,7 @@ class _SettingsScreenV2State extends ConsumerState { Future _resetAndClearData() async { if (FeatureFlags.enableRemoteNotifications) { - try { - ref.read(firebaseMessagingServiceProvider).unregisterDevice(); - } catch (e) { - debugPrint('Failed to unregister device (non-fatal): $e'); - } + ref.read(firebaseMessagingServiceProvider).unregisterDevice(); } _settingsService.clearAll();