// Friendly-error mapper. Turns a thrown object (typically a // `GrpcError` from package:grpc) into a short human sentence // plus an optional recovery hint, ready to drop into the UI. // // Why this exists: Studio used to render `GrpcError.toString()` // verbatim — operators saw walls of text like // "gRPC Error (code: 13, codeName: INTERNAL, message: ..., details: [], rawResponse: null, trailers: {...})". // The friendly mapper folds every gRPC code into a one-line // explanation we can localise, plus a recovery action when one // is obvious (open settings, restart hub, retry). import '../l10n/app_localizations.dart'; /// Result of running an error through [friendlyError]. The /// `headline` is the short user-facing sentence; `detail` is /// the verbatim original error string (kept around in case the /// operator wants to copy-paste it into a bug report). `hint` /// is a one-sentence recovery suggestion or `null`. class FriendlyError { final String headline; final String detail; final String? hint; const FriendlyError({ required this.headline, required this.detail, this.hint, }); } /// Map an arbitrary thrown object to a [FriendlyError]. Always /// returns a value — never throws — so callers can drop the /// result straight into UI without try/catch ceremony. FriendlyError friendlyError(Object error, AppLocalizations l) { // We deliberately don't import package:grpc here so Studio // doesn't have to add it to its own pubspec — the dependency // lives one layer down in fai_dart_sdk. `GrpcError` has a // stable `.code` (int) and `.message` (String?) shape; we // duck-type on those instead of an `is` check. final code = _intField(error, 'code'); final detail = _stringField(error, 'message') ?? ''; if (code != null) { switch (code) { case 3: // INVALID_ARGUMENT return FriendlyError( headline: l.errInvalidArgument, detail: detail, hint: l.errInvalidArgumentHint, ); case 5: // NOT_FOUND return FriendlyError( headline: l.errNotFound, detail: detail, hint: l.errNotFoundHint, ); case 6: // ALREADY_EXISTS return FriendlyError( headline: l.errAlreadyExists, detail: detail, hint: null, ); case 7: // PERMISSION_DENIED return FriendlyError( headline: l.errPermissionDenied, detail: detail, hint: l.errPermissionDeniedHint, ); case 9: // FAILED_PRECONDITION return FriendlyError( headline: l.errFailedPrecondition, detail: detail, hint: l.errFailedPreconditionHint, ); case 13: // INTERNAL return FriendlyError( headline: l.errInternal, detail: detail, hint: l.errInternalHint, ); case 14: // UNAVAILABLE return FriendlyError( headline: l.errUnavailable, detail: detail, hint: l.errUnavailableHint, ); case 16: // UNAUTHENTICATED return FriendlyError( headline: l.errUnauthenticated, detail: detail, hint: l.errUnauthenticatedHint, ); default: final codeName = _stringField(error, 'codeName') ?? 'gRPC $code'; return FriendlyError( headline: '$codeName: ${detail.isEmpty ? l.errGeneric : detail}', detail: detail, hint: null, ); } } // Non-gRPC error (e.g. FormatException from a bad URL). Show // its toString — but as the headline, not buried in a wall. return FriendlyError( headline: error.toString(), detail: error.toString(), hint: null, ); } /// Try to read an `int` field by name off an arbitrary object. /// Returns `null` when the field doesn't exist or has another /// runtime type. Used to duck-type `GrpcError.code` without /// pulling package:grpc as a Studio dependency. int? _intField(Object obj, String field) { try { final dyn = obj as dynamic; // ignore: avoid_dynamic_calls final v = (() { switch (field) { case 'code': return dyn.code; } return null; })(); return v is int ? v : null; } catch (_) { return null; } } String? _stringField(Object obj, String field) { try { final dyn = obj as dynamic; // ignore: avoid_dynamic_calls final v = (() { switch (field) { case 'message': return dyn.message; case 'codeName': return dyn.codeName; } return null; })(); return v is String ? v : null; } catch (_) { return null; } }