From 2369f7f0b6e05c22e12e5ecece3f5a76cd52ba16 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 25 Dec 2025 16:27:49 -0600 Subject: [PATCH 1/4] feat: add 3-second debounce timer for Mostro instance restore - Add Timer to prevent immediate session restoration while typing - Only trigger restore 3 seconds after user stops typing - Cancel timer properly in dispose to prevent memory leaks --- lib/features/settings/settings_screen.dart | 26 +++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index a078a8f1..63466816 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -23,6 +24,7 @@ class SettingsScreen extends ConsumerStatefulWidget { class _SettingsScreenState extends ConsumerState { late final TextEditingController _mostroTextController; late final TextEditingController _lightningAddressController; + Timer? _debounceTimer; @override void initState() { @@ -35,6 +37,7 @@ class _SettingsScreenState extends ConsumerState { @override void dispose() { + _debounceTimer?.cancel(); _mostroTextController.dispose(); _lightningAddressController.dispose(); super.dispose(); @@ -462,18 +465,21 @@ class _SettingsScreenState extends ConsumerState { controller: controller, style: const TextStyle(color: AppTheme.textPrimary), onChanged: (value) async { - final oldValue = ref.read(settingsProvider).mostroPublicKey; - await ref.read(settingsProvider.notifier).updateMostroInstance(value); + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(seconds: 3), () async { + final oldValue = ref.read(settingsProvider).mostroPublicKey; + await ref.read(settingsProvider.notifier).updateMostroInstance(value); - // Trigger restore if pubkey changed - if (oldValue != value && value.isNotEmpty) { - try { - final restoreService = ref.read(restoreServiceProvider); - await restoreService.initRestoreProcess(); - } catch (e) { - // Ignore errors during restore + // Trigger restore if pubkey changed + if (oldValue != value && value.isNotEmpty) { + try { + final restoreService = ref.read(restoreServiceProvider); + await restoreService.initRestoreProcess(); + } catch (e) { + // Ignore errors during restore + } } - } + }); }, decoration: InputDecoration( border: InputBorder.none, From 2bb7c7ea4490f3bb76decd53a1ca071ce393fff1 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 25 Dec 2025 17:00:59 -0600 Subject: [PATCH 2/4] feat: add pubkey validation and npub to hex conversion - Add format validation for Mostro public keys (hex and npub) - Auto-convert npub format to hex before saving - Display localized error messages for invalid formats - Support both hex (64 chars) and npub1 (63 chars) formats - Show converted hex in field after npub input --- lib/features/settings/settings_screen.dart | 85 +++++++++++++++++++--- lib/l10n/intl_en.arb | 5 +- lib/l10n/intl_es.arb | 4 +- lib/l10n/intl_it.arb | 5 +- 4 files changed, 87 insertions(+), 12 deletions(-) diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index 63466816..b28c798a 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -12,6 +12,7 @@ import 'package:mostro_mobile/features/restore/restore_manager.dart'; import 'package:mostro_mobile/shared/widgets/currency_selection_dialog.dart'; import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; import 'package:mostro_mobile/shared/widgets/language_selector.dart'; +import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; import 'package:mostro_mobile/generated/l10n.dart'; class SettingsScreen extends ConsumerStatefulWidget { @@ -25,6 +26,7 @@ class _SettingsScreenState extends ConsumerState { late final TextEditingController _mostroTextController; late final TextEditingController _lightningAddressController; Timer? _debounceTimer; + String? _pubkeyError; @override void initState() { @@ -43,6 +45,31 @@ class _SettingsScreenState extends ConsumerState { super.dispose(); } + bool _isValidPubkey(String input) { + // Validate hex format (64 characters, hex only) + if (input.length == 64 && RegExp(r'^[a-fA-F0-9]+$').hasMatch(input)) { + return true; + } + // Validate npub format (63 characters, starts with npub1) + if (input.startsWith('npub1') && input.length == 63) { + return true; + } + return false; + } + + String _convertToHex(String input) { + try { + if (input.startsWith('npub1')) { + // Use dart_nostr Bech32 decoding + final decoded = NostrUtils.decodeBech32(input); + return decoded; + } + return input; // Already hex format + } catch (e) { + throw const FormatException('Invalid npub format'); + } + } + @override Widget build(BuildContext context) { // Listen to settings changes and update controllers @@ -467,16 +494,46 @@ class _SettingsScreenState extends ConsumerState { onChanged: (value) async { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(seconds: 3), () async { - final oldValue = ref.read(settingsProvider).mostroPublicKey; - await ref.read(settingsProvider.notifier).updateMostroInstance(value); + if (!mounted) return; + + try { + if (_isValidPubkey(value)) { + setState(() { + _pubkeyError = null; + }); + + final hexValue = _convertToHex(value); + final oldValue = ref.read(settingsProvider).mostroPublicKey; + await ref.read(settingsProvider.notifier).updateMostroInstance(hexValue); - // Trigger restore if pubkey changed - if (oldValue != value && value.isNotEmpty) { - try { - final restoreService = ref.read(restoreServiceProvider); - await restoreService.initRestoreProcess(); - } catch (e) { - // Ignore errors during restore + // Update text controller to show hex if it was npub + if (value.startsWith('npub1')) { + controller.text = hexValue; + } + + // Trigger restore if pubkey changed + if (oldValue != hexValue && hexValue.isNotEmpty) { + try { + final restoreService = ref.read(restoreServiceProvider); + await restoreService.initRestoreProcess(); + } catch (e) { + // Ignore errors during restore + } + } + } else if (value.isNotEmpty) { + setState(() { + _pubkeyError = S.of(context)!.invalidKeyFormat; + }); + } else { + setState(() { + _pubkeyError = null; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _pubkeyError = S.of(context)!.invalidKeyFormat; + }); } } }); @@ -492,6 +549,16 @@ class _SettingsScreenState extends ConsumerState { ), ), ), + if (_pubkeyError != null) ...[ + const SizedBox(height: 8), + Text( + _pubkeyError!, + style: const TextStyle( + color: Colors.red, + fontSize: 12, + ), + ), + ], ], ), ), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index aa255c64..c2d78f70 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1217,5 +1217,8 @@ "@_comment_backup_reminder": "Backup reminder notification", "backupAccountReminder": "You must back up your account", - "backupAccountReminderMessage": "Back up your secret words to recover your account" + "backupAccountReminderMessage": "Back up your secret words to recover your account", + + "@_comment_validation": "Input validation messages", + "invalidKeyFormat": "Invalid key format" } \ No newline at end of file diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index f97ffa0b..7bfe9749 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1195,6 +1195,8 @@ "@_comment_backup_reminder": "Recordatorio de respaldo de cuenta", "backupAccountReminder": "Debes respaldar tu cuenta", - "backupAccountReminderMessage": "Respalda tus palabras secretas para recuperar tu cuenta" + "backupAccountReminderMessage": "Respalda tus palabras secretas para recuperar tu cuenta", + "@_comment_validation": "Mensajes de validación de entrada", + "invalidKeyFormat": "Formato de clave inválido" } \ No newline at end of file diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 5ca41f24..21d50225 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1250,5 +1250,8 @@ "@_comment_backup_reminder": "Promemoria backup account", "backupAccountReminder": "Devi fare il backup del tuo account", - "backupAccountReminderMessage": "Salva le tue parole segrete per recuperare il tuo account" + "backupAccountReminderMessage": "Salva le tue parole segrete per recuperare il tuo account", + + "@_comment_validation": "Messaggi di convalida input", + "invalidKeyFormat": "Formato chiave non valido" } \ No newline at end of file From f6d759eaf1f2921efbedf2a25ccd93cf79daa0e0 Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Thu, 25 Dec 2025 17:07:07 -0600 Subject: [PATCH 3/4] update Mostro info text to mention hex/npub format support --- lib/l10n/intl_en.arb | 2 +- lib/l10n/intl_es.arb | 2 +- lib/l10n/intl_it.arb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c2d78f70..edf2ea86 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -904,7 +904,7 @@ "currencyInfoText": "Choose the default currency for your orders.", "lightningAddressInfoText": "A Lightning Address is an email-like identifier (e.g., user@domain.com) that simplifies receiving Bitcoin payments. Setting a default one here will automatically populate it when creating buy orders, making the process faster and more convenient.", "relaysInfoText": "• Relays are servers that help distribute your messages across the Nostr network.\n\n• Adding more relays can improve connectivity and redundancy.\n\n• Relays don't sync with each other, so only those you're connected to will receive your messages.", - "mostroInfoText": "• The Mostro you select will be the one where you post your offers.\n\n• In case of a dispute, a human assigned to that Mostro will be the one to resolve it.", + "mostroInfoText": "• Supports hex and npub formats (displays as hex).\n\n• The Mostro you select will be the one where you post your offers.\n\n• In case of a dispute, a human assigned to that Mostro will be the one to resolve it.", "@_comment_account_info_dialogs": "Account Screen Info Dialog Text", "secretWordsInfoText": "• These are your secret words, the only way to recover your account if you lose access to this app or want to use your identity in another app.\n\n• Write them down carefully and store them in a secure, private location. Never share them with anyone.\n\n• If you lose these words, you'll permanently lose access to your account.", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 7bfe9749..68069133 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -883,7 +883,7 @@ "currencyInfoText": "Elige la moneda predeterminada para tus órdenes.", "lightningAddressInfoText": "Una Dirección Lightning es un identificador similar a un email (ej., usuario@dominio.com) que simplifica recibir pagos de Bitcoin. Establecer una predeterminada aquí la completará automáticamente al crear órdenes de compra, haciendo el proceso más rápido y conveniente.", "relaysInfoText": "• Los relays son servidores que ayudan a distribuir tus mensajes a través de la red Nostr.\n\n• Agregar más relays puede mejorar la conectividad y redundancia.\n\n• Los relays no se sincronizan entre sí, por lo que solo aquellos a los que estés conectado recibirán tus mensajes.", - "mostroInfoText": "• El Mostro que selecciones será donde publiques tus ofertas.\n\n• En caso de disputa, una persona asignada a ese Mostro será quien la resuelva.", + "mostroInfoText": "• Soporta formatos hex y npub (se muestra como hex).\n\n• El Mostro que selecciones será donde publiques tus ofertas.\n\n• En caso de disputa, una persona asignada a ese Mostro será quien la resuelva.", "@_comment_account_info_dialogs": "Texto de Diálogos de Información de Cuenta", "secretWordsInfoText": "• Estas son tus palabras secretas, la única forma de recuperar tu cuenta si pierdes acceso a esta aplicación o quieres usar tu identidad en otra aplicación.\n\n• Escríbelas cuidadosamente y guárdalas en un lugar seguro y privado. Nunca las compartas con nadie.\n\n• Si pierdes estas palabras, perderás permanentemente el acceso a tu cuenta.", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 21d50225..566cdc4c 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -939,7 +939,7 @@ "currencyInfoText": "Scegli la valuta predefinita per i tuoi ordini.", "lightningAddressInfoText": "Un Indirizzo Lightning è un identificatore simile a un'email (es., utente@dominio.com) che semplifica la ricezione di pagamenti Bitcoin. Impostarne uno predefinito qui lo completerà automaticamente durante la creazione di ordini di acquisto, rendendo il processo più veloce e conveniente.", "relaysInfoText": "• I relay sono server che aiutano a distribuire i tuoi messaggi attraverso la rete Nostr.\n\n• Aggiungere più relay può migliorare la connettività e la ridondanza.\n\n• I relay non si sincronizzano tra loro, quindi solo quelli a cui sei connesso riceveranno i tuoi messaggi.", - "mostroInfoText": "• Il Mostro che selezioni sarà quello dove pubblicherai le tue offerte.\n\n• In caso di disputa, una persona assegnata a quel Mostro sarà quella che la risolverà.", + "mostroInfoText": "• Supporta formati hex e npub (mostra come hex).\n\n• Il Mostro che selezioni sarà quello dove pubblicherai le tue offerte.\n\n• In caso di disputa, una persona assegnata a quel Mostro sarà quella che la risolverà.", "@_comment_account_info_dialogs": "Testo Dialoghi Informazioni Account", "secretWordsInfoText": "• Queste sono le tue parole segrete, l'unico modo per recuperare il tuo account se perdi l'accesso a questa app o vuoi usare la tua identità in un'altra app.\n\n• Scrivile attentamente e conservale in un luogo sicuro e privato. Non condividerle mai con nessuno.\n\n• Se perdi queste parole, perderai permanentemente l'accesso al tuo account.", From 07e105ec7bf2f331fd56c199f194e15235e2b0da Mon Sep 17 00:00:00 2001 From: Catrya <140891948+Catrya@users.noreply.github.com> Date: Fri, 26 Dec 2025 11:54:21 -0600 Subject: [PATCH 4/4] Add mounted check before updating controller after async operation --- lib/features/settings/settings_screen.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index b28c798a..454d4d02 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -506,6 +506,9 @@ class _SettingsScreenState extends ConsumerState { final oldValue = ref.read(settingsProvider).mostroPublicKey; await ref.read(settingsProvider.notifier).updateMostroInstance(hexValue); + // Check mounted after async operation + if (!mounted) return; + // Update text controller to show hex if it was npub if (value.startsWith('npub1')) { controller.text = hexValue;