Skip to content

Commit b81808e

Browse files
committed
fix(labels): stop leaking raw error detail in label messages
LabelError becomes a sealed family with per-variant toTranslated. The unexpected catch-all now returns a generic localized message instead of the raw exception text, and the label usecases log the technical reason at the mapping site.
1 parent 64e5bcc commit b81808e

6 files changed

Lines changed: 75 additions & 35 deletions

File tree

lib/features/labels/application/usecases/fetch_all_labels_usecase.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ class FetchAllLabelsUsecase {
1717
.toList();
1818
} on LabelError {
1919
rethrow;
20-
} catch (e) {
21-
log.severe(error: e, trace: StackTrace.current);
22-
throw LabelError.unexpected('Failed to fetch all labels: $e');
20+
} catch (e, st) {
21+
// Keep the technical reason in the logs; the UI maps the unexpected
22+
// variant to a generic message and never shows [e].
23+
log.severe(message: 'Failed to fetch all labels', error: e, trace: st);
24+
throw UnexpectedLabelError('Failed to fetch all labels: $e');
2325
}
2426
}
2527
}

lib/features/labels/application/usecases/fetch_label_by_reference_usecase.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:bb_mobile/core/utils/logger.dart';
12
import 'package:bb_mobile/features/labels/adapters/label_mapper.dart';
23
import 'package:bb_mobile/features/labels/application/application_label.dart';
34
import 'package:bb_mobile/features/labels/application/labels_repository_port.dart';
@@ -16,10 +17,13 @@ class FetchLabelByReferenceUsecase {
1617
.toList();
1718
} on LabelError {
1819
rethrow;
19-
} catch (e) {
20-
throw LabelError.unexpected(
21-
'Failed to fetch label for reference $reference: $e',
22-
);
20+
} catch (e, st) {
21+
// Keep the technical reason in the logs; the UI maps the unexpected
22+
// variant to a generic message and never shows [e]. The reference is a
23+
// txid/address/xpub — keep it OUT of the Sentry-bound message to avoid
24+
// leaking an on-chain identifier off-device.
25+
log.severe(message: 'Failed to fetch label by reference', error: e, trace: st);
26+
throw UnexpectedLabelError('Failed to fetch label by reference: $e');
2327
}
2428
}
2529
}

lib/features/labels/application/usecases/store_labels_usecase.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:bb_mobile/core/utils/logger.dart';
12
import 'package:bb_mobile/features/labels/adapters/label_mapper.dart';
23
import 'package:bb_mobile/features/labels/application/application_label.dart';
34
import 'package:bb_mobile/features/labels/application/labels_repository_port.dart';
@@ -22,8 +23,11 @@ class StoreLabelUsecase {
2223
return LabelMapper.labelEntityToApplicationLabel(storedLabel);
2324
} on LabelError {
2425
rethrow;
25-
} catch (e) {
26-
throw LabelError.unexpected('Failed to batch labels: $e');
26+
} catch (e, st) {
27+
// Keep the technical reason in the logs; the UI maps the unexpected
28+
// variant to a generic message and never shows [e].
29+
log.severe(message: 'Failed to store label', error: e, trace: st);
30+
throw UnexpectedLabelError('Failed to store label: $e');
2731
}
2832
}
2933
}

lib/features/labels/application/usecases/trash_label_usecase.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:bb_mobile/core/utils/logger.dart';
12
import 'package:bb_mobile/features/labels/application/labels_repository_port.dart';
23
import 'package:bb_mobile/features/labels/domain/label_error.dart';
34

@@ -11,8 +12,11 @@ class TrashLabelUsecase {
1112
await _labelRepository.trash(id);
1213
} on LabelError {
1314
rethrow;
14-
} catch (e) {
15-
throw LabelError.unexpected('Failed to trash label $id: $e');
15+
} catch (e, st) {
16+
// Keep the technical reason in the logs; the UI maps the unexpected
17+
// variant to a generic message and never shows [e].
18+
log.severe(message: 'Failed to trash label $id', error: e, trace: st);
19+
throw UnexpectedLabelError('Failed to trash label $id: $e');
1620
}
1721
}
1822
}
Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,51 @@
1-
import 'package:freezed_annotation/freezed_annotation.dart';
2-
3-
part 'label_error.freezed.dart';
4-
5-
@freezed
6-
sealed class LabelError with _$LabelError {
7-
const factory LabelError.notFound({required String label}) = LabelNotFound;
8-
const factory LabelError.unsupportedType(Type runtimeType) =
9-
UnsupportedLabelType;
10-
const factory LabelError.unexpected(String? message) = UnexpectedLabelError;
11-
const factory LabelError.systemLabelCannotBeDeleted() =
12-
SystemLabelCannotBeDeletedError;
13-
const LabelError._();
1+
import 'package:bb_mobile/core/utils/build_context_x.dart';
2+
import 'package:flutter/widgets.dart';
3+
4+
/// Closed set of every failure the labels feature surfaces to the user.
5+
/// `sealed` keeps it closed (no foreign variants; exhaustive switches). The
6+
/// abstract `toTranslated` makes a user-facing message mandatory and keeps it
7+
/// next to its variant.
8+
sealed class LabelError {
9+
const LabelError();
10+
11+
/// Localized, user-safe message. Never returns raw/technical detail.
12+
String toTranslated(BuildContext context);
13+
}
14+
15+
final class LabelNotFound extends LabelError {
16+
final String label;
17+
18+
const LabelNotFound({required this.label});
19+
20+
@override
21+
String toTranslated(BuildContext context) =>
22+
context.loc.labelErrorNotFound(label);
23+
}
24+
25+
final class UnsupportedLabelType extends LabelError {
26+
const UnsupportedLabelType();
27+
28+
@override
29+
String toTranslated(BuildContext context) =>
30+
context.loc.labelErrorUnsupportedType;
31+
}
32+
33+
final class SystemLabelCannotBeDeletedError extends LabelError {
34+
const SystemLabelCannotBeDeletedError();
35+
36+
@override
37+
String toTranslated(BuildContext context) =>
38+
context.loc.labelErrorSystemCannotDelete;
39+
}
40+
41+
/// Catch-all. [message] is for logs only and MUST never reach the UI —
42+
/// `toTranslated` returns the shared generic string, not [message].
43+
final class UnexpectedLabelError extends LabelError {
44+
final String? message;
45+
46+
const UnexpectedLabelError(this.message);
47+
48+
@override
49+
String toTranslated(BuildContext context) =>
50+
context.loc.oopsSomethingWentWrong;
1451
}

lib/features/labels/ui/labels_widget.dart

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,6 @@ import 'package:bb_mobile/features/labels/labels_facade.dart';
77
import 'package:bb_mobile/locator.dart';
88
import 'package:flutter/material.dart';
99

10-
extension LabelErrorTranslation on LabelError {
11-
String toTranslated(BuildContext context) => when(
12-
notFound: (label) => context.loc.labelErrorNotFound(label),
13-
unsupportedType: (type) =>
14-
context.loc.labelErrorUnsupportedType(type.toString()),
15-
unexpected: (message) =>
16-
message != null ? context.loc.labelErrorUnexpected(message) : '',
17-
systemLabelCannotBeDeleted: () => context.loc.labelErrorSystemCannotDelete,
18-
);
19-
}
20-
2110
class LabelsWidget extends StatefulWidget {
2211
const LabelsWidget({super.key, required this.labels, this.onDelete});
2312

0 commit comments

Comments
 (0)