diff --git a/lib/data/models/last_trade_index_response.dart b/lib/data/models/last_trade_index_response.dart index 2a3394054..cf914576d 100644 --- a/lib/data/models/last_trade_index_response.dart +++ b/lib/data/models/last_trade_index_response.dart @@ -2,8 +2,12 @@ import 'package:mostro_mobile/data/models/payload.dart'; class LastTradeIndexResponse implements Payload { final int tradeIndex; + final bool noHistoryFound; - const LastTradeIndexResponse({required this.tradeIndex}); + const LastTradeIndexResponse({ + required this.tradeIndex, + this.noHistoryFound = false, + }); @override String get type => 'last-trade-index'; diff --git a/lib/features/restore/restore_manager.dart b/lib/features/restore/restore_manager.dart index 2c95478fe..af076f99e 100644 --- a/lib/features/restore/restore_manager.dart +++ b/lib/features/restore/restore_manager.dart @@ -8,6 +8,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/data/models/enums/cant_do_reason.dart'; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; @@ -439,12 +440,27 @@ class RestoreService { final contentList = jsonDecode(rumor.content!) as List; final messageData = contentList[0] as Map; - // Check if Mostro returned cant-do (not found) + // Check if Mostro returned cant-do if (messageData.containsKey('cant-do')) { - logger.w( - 'Restore: Mostro returned cant-do for last trade index, defaulting to 0', + final cantDoWrapper = messageData['cant-do'] as Map?; + final cantDoPayload = cantDoWrapper?['payload'] as Map?; + final reasonStr = _extractCantDoReason(cantDoPayload); + if (reasonStr == CantDoReason.invalidTradeIndex.value) { + logger.w( + 'Restore: Mostro returned cant-do: invalid_trade_index for last trade index', + ); + throw const RestoreInvalidTradeIndexException(); + } + if (reasonStr == CantDoReason.notFound.value) { + logger.w('Restore: no previous trade history found'); + return const LastTradeIndexResponse( + tradeIndex: 0, + noHistoryFound: true, + ); + } + throw Exception( + 'Restore: cant-do on last trade index (reason: ${reasonStr ?? 'unknown'})', ); - return LastTradeIndexResponse(tradeIndex: 0); } // Extract trade_index from restore wrapper @@ -840,6 +856,7 @@ class RestoreService { _operationInProgress = true; _operationCompleter = Completer(); bool success = false; + bool noHistoryFound = false; try { // Clear existing data await _clearAll(); @@ -895,6 +912,7 @@ class RestoreService { ); final lastTradeIndex = lastTradeIndexResponse.tradeIndex; await keyManager.setCurrentKeyIndex(lastTradeIndex + 1); + noHistoryFound = lastTradeIndexResponse.noHistoryFound; success = true; return true; } @@ -920,6 +938,7 @@ class RestoreService { lastTradeIndexEvent, ); final lastTradeIndex = lastTradeIndexResponse.tradeIndex; + noHistoryFound = lastTradeIndexResponse.noHistoryFound; // IMPORTANT: Cancel temporary subscription before proceeding to avoid interference await _tempSubscription?.cancel(); @@ -944,11 +963,12 @@ class RestoreService { error: e, stackTrace: stack, ); + final errorKey = e is RestoreInvalidTradeIndexException + ? 'invalid_trade_index' + : 'restore_error'; ref .read(restoreProgressProvider.notifier) - .showError( - 'restore_error', - ); // overlay displays localized restoreErrorMessage + .showError(errorKey); // overlay maps the key to a localized message } finally { // Cleanup: always cancel subscription and clear keys logger.i('Restore: cleaning up subscription and keys'); @@ -964,7 +984,11 @@ class RestoreService { // Only call completeRestore if not in error state final currentState = ref.read(restoreProgressProvider); if (currentState.step != RestoreStep.error) { - ref.read(restoreProgressProvider.notifier).completeRestore(); + if (noHistoryFound) { + ref.read(restoreProgressProvider.notifier).completeAsNewUser(); + } else { + ref.read(restoreProgressProvider.notifier).completeRestore(); + } } } @@ -1027,6 +1051,30 @@ class RestoreService { _operationCompleter = null; } } + + // Extracts the cant-do reason string from the inner payload. + // Mostro wire format: payload may be {"cant_do": ""} or + // {"cant_do": {"cant-do": ""}}. + String? _extractCantDoReason(Map? payload) { + if (payload == null) return null; + final raw = payload['cant_do']; + if (raw == null) return null; + if (raw is String) return raw; + if (raw is Map) { + final inner = raw['cant-do']; + return inner?.toString(); + } + return null; + } +} + +/// Thrown when Mostro responds with cant-do: invalid_trade_index to +/// Action.lastTradeIndex during the restore flow. +class RestoreInvalidTradeIndexException implements Exception { + const RestoreInvalidTradeIndexException(); + + @override + String toString() => 'RestoreInvalidTradeIndexException'; } final restoreServiceProvider = Provider((ref) { diff --git a/lib/features/restore/restore_overlay.dart b/lib/features/restore/restore_overlay.dart index e0988b044..bf47cc8c3 100644 --- a/lib/features/restore/restore_overlay.dart +++ b/lib/features/restore/restore_overlay.dart @@ -63,6 +63,32 @@ class RestoreOverlay extends ConsumerWidget { ), textAlign: TextAlign.center, ), + if (state.step == RestoreStep.completed && + state.noHistoryFound) ...[ + SizedBox(height: isSmallScreen ? 6 : 8), + Text( + S.of(context)!.restoreNoPreviousData, + style: TextStyle( + color: AppTheme.textSecondary.withValues(alpha: 0.75), + fontSize: messageFontSize - 1, + fontStyle: FontStyle.italic, + ), + textAlign: TextAlign.center, + ), + ], + if (state.step == RestoreStep.error && + state.errorMessage == 'invalid_trade_index') ...[ + SizedBox(height: isSmallScreen ? 6 : 8), + Text( + S.of(context)!.restoreInvalidTradeIndex, + style: TextStyle( + color: AppTheme.textSecondary.withValues(alpha: 0.75), + fontSize: messageFontSize - 1, + fontStyle: FontStyle.italic, + ), + textAlign: TextAlign.center, + ), + ], SizedBox(height: isSmallScreen ? 16 : 24), _buildProgressIndicator(state, iconSize), ], diff --git a/lib/features/restore/restore_progress_notifier.dart b/lib/features/restore/restore_progress_notifier.dart index 5a49d3ab9..f695dc1db 100644 --- a/lib/features/restore/restore_progress_notifier.dart +++ b/lib/features/restore/restore_progress_notifier.dart @@ -90,6 +90,26 @@ class RestoreProgressNotifier extends StateNotifier { }); } + void completeAsNewUser() { + if (!_canUpdateState || state.step == RestoreStep.completed) return; + + logger.i('Restore completed: no previous history for this Mostro'); + _cancelTimeoutTimer(); + + _setStateSafely(state.copyWith( + step: RestoreStep.completed, + noHistoryFound: true, + )); + + // Extended auto-hide so the user has time to read the secondary line. + _cancelAutoHideTimer(); + _autoHideTimer = Timer(const Duration(seconds: 5), () { + if (mounted) { + hide(); + } + }); + } + void showError(String message) { if (!_canUpdateState || state.step == RestoreStep.error) return; diff --git a/lib/features/restore/restore_progress_state.dart b/lib/features/restore/restore_progress_state.dart index fe9127d60..b63d6b45a 100644 --- a/lib/features/restore/restore_progress_state.dart +++ b/lib/features/restore/restore_progress_state.dart @@ -14,6 +14,7 @@ class RestoreProgressState { final int totalProgress; final String? errorMessage; final bool isVisible; + final bool noHistoryFound; const RestoreProgressState({ required this.step, @@ -21,6 +22,7 @@ class RestoreProgressState { this.totalProgress = 0, this.errorMessage, this.isVisible = false, + this.noHistoryFound = false, }); RestoreProgressState copyWith({ @@ -29,6 +31,7 @@ class RestoreProgressState { int? totalProgress, String? errorMessage, bool? isVisible, + bool? noHistoryFound, }) { return RestoreProgressState( step: step ?? this.step, @@ -36,6 +39,7 @@ class RestoreProgressState { totalProgress: totalProgress ?? this.totalProgress, errorMessage: errorMessage ?? this.errorMessage, isVisible: isVisible ?? this.isVisible, + noHistoryFound: noHistoryFound ?? this.noHistoryFound, ); } diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index bd73d6ad9..32f591372 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -1243,8 +1243,10 @@ "restoreProcessingRoles": "Rollen werden verarbeitet...", "restoreFinalizing": "Wiederherstellung wird abgeschlossen...", "restoreCompleted": "Wiederherstellung abgeschlossen!", + "restoreNoPreviousData": "Keine vorherigen Daten für diesen Nutzer in diesem Mostro", "restoreError": "Wiederherstellungsfehler", "restoreErrorMessage": "Fehler beim Wiederherstellen der Nutzerdaten. Bitte prüfe deine Verbindung und versuche es erneut.", + "restoreInvalidTradeIndex": "Ungültiger Handelsindex", "@_comment_file_messaging": "Dateiversand-Strings", "download": "Herunterladen", "openFile": "Datei öffnen", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 220266fc2..2867146fc 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1243,8 +1243,10 @@ "restoreProcessingRoles": "Processing roles...", "restoreFinalizing": "Finalizing restore...", "restoreCompleted": "Restore completed!", + "restoreNoPreviousData": "No previous data for this user in this Mostro", "restoreError": "Restore error", "restoreErrorMessage": "Error restoring user data. Please check your connection and try again.", + "restoreInvalidTradeIndex": "Invalid trade index", "@_comment_file_messaging": "File messaging strings", "download": "Download", "openFile": "Open File", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 1491ccb6b..725648b50 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1218,8 +1218,10 @@ "restoreProcessingRoles": "Procesando roles...", "restoreFinalizing": "Finalizando restauración...", "restoreCompleted": "¡Restauración completada!", + "restoreNoPreviousData": "No hay datos previos de este usuario en este Mostro", "restoreError": "Error de restauración", "restoreErrorMessage": "Error al restaurar datos del usuario. Verifica tu conexión e inténtalo más tarde.", + "restoreInvalidTradeIndex": "Índice de intercambio no válido", "@_comment_file_messaging": "File messaging strings", "download": "Descargar", "openFile": "Abrir Archivo", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 8970a47d6..984a0d140 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1243,8 +1243,10 @@ "restoreProcessingRoles": "Traitement des rôles...", "restoreFinalizing": "Finalisation de la restauration...", "restoreCompleted": "Restauration terminée !", + "restoreNoPreviousData": "Aucune donnée précédente pour cet utilisateur sur ce Mostro", "restoreError": "Erreur de restauration", "restoreErrorMessage": "Erreur lors de la restauration des données utilisateur. Veuillez vérifier votre connexion et réessayer.", + "restoreInvalidTradeIndex": "Index d'échange invalide", "@_comment_file_messaging": "File messaging strings", "download": "Télécharger", "openFile": "Ouvrir le fichier", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 9a77b72c5..552e385e7 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1284,8 +1284,10 @@ "restoreProcessingRoles": "Elaborazione ruoli...", "restoreFinalizing": "Finalizzazione ripristino...", "restoreCompleted": "Ripristino completato!", + "restoreNoPreviousData": "Nessun dato precedente per questo utente in questo Mostro", "restoreError": "Errore di ripristino", "restoreErrorMessage": "Errore nel ripristino dei dati utente. Verifica la tua connessione e riprova più tardi.", + "restoreInvalidTradeIndex": "Indice di scambio non valido", "@_comment_file_messaging": "File messaging strings", "download": "Scarica", "openFile": "Apri File",