fix: throw instead of silent failure leading to invalid enum value returns. And some other cleanup
What changed, and why it matters
This commit changes how the Stack Wallet app handles unexpected status values from its ShopInBit partner service. Previously, if the server sent an unknown status string, the app silently replaced it with a default value (e.g., treating an unknown ticket state as a brand-new ticket). Now the app throws an exception instead. The commit also moves some UI label/color code into the enum definitions and removes duplicate code. The change is a defensive fix that prevents the app from misrepresenting order or webhook states, but it could also cause crashes if the server ever introduces new status values that older app versions don't recognize.
Treat as a hardening/defensive fix. Review whether callers of `TicketState.fromString`, `WebhookEventType.fromString`, and `_toInt` handle exceptions gracefully to avoid unhandled crashes, especially for webhook processing and order detail rendering. Consider adding structured logging or a fallback UI state for unknown future values rather than propagating raw exceptions to users. No immediate exploit mitigation is required.
Security signals we found
Silent fallback to default enum values removed in favor of explicit exceptions
Unknown server-provided state strings now raise errors instead of being coerced
Defensive parsing change in `_toInt` from silent default `0` to throwing `int.parse`
UI label/color logic deduplicated into enum getters (cleanup, not security)
Commit title self-describes the change as fixing silent failure / invalid enum returns
Evidence from the diff
The patch modifies enum deserialization in TicketState.fromString() and WebhookEventType.fromString() so that unknown string values throw an exception rather than falling back to a default enum value (TicketState.newTicket / WebhookEventType.ticketStateChanged). It also refactors ShopInBitOrderStatus to include label and getColor() getters, and updates two UI files to use these getters instead of local switch helpers. Additionally, _toInt() was tightened: it previously returned 0 for unsupported types, but now calls int.parse(value.toString()), which will throw for non-numeric values. The commit title explicitly frames this as replacing ‘silent failure leading to invalid enum value returns’ with throwing behavior.
Changed components
lib/services/shopinbit/src/models/ticket.dartlib/services/shopinbit/src/models/webhook_event.dartlib/models/shopinbit/shopinbit_order_model.dartlib/pages/shopinbit/shopinbit_ticket_detail.dartlib/pages/shopinbit/shopinbit_tickets_view.dartInspect captured patch +57 / −113
diff --git a/lib/models/shopinbit/shopinbit_order_model.dart b/lib/models/shopinbit/shopinbit_order_model.dart
index f41aa49..88d247c 100644
--- a/lib/models/shopinbit/shopinbit_order_model.dart
+++ b/lib/models/shopinbit/shopinbit_order_model.dart
@@ -1,6 +1,9 @@
+import 'dart:ui';
+
import 'package:flutter/foundation.dart';
import '../../services/shopinbit/src/models/ticket.dart';
+import '../../themes/stack_colors.dart';
import '../isar/models/shopinbit_ticket.dart';
enum ShopInBitCategory { concierge, travel, car }
@@ -16,7 +19,29 @@ enum ShopInBitOrderStatus {
delivered,
closed,
cancelled,
- refunded,
+ refunded;
+
+ String get label => switch (this) {
+ .pending => "Pending",
+ .reviewing => "Under review",
+ .offerAvailable => "Offer available",
+ .accepted => "Accepted",
+ .paymentPending => "Awaiting payment",
+ .paid => "Paid",
+ .shipping => "Shipping",
+ .delivered => "Delivered",
+ .closed => "Closed",
+ .cancelled => "Cancelled",
+ .refunded => "Refunded",
+ };
+
+ Color getColor(StackColors colors) => switch (this) {
+ .delivered => colors.accentColorGreen,
+ .offerAvailable => colors.accentColorBlue,
+ .pending || .reviewing => colors.accentColorYellow,
+ .closed || .cancelled || .refunded => colors.textSubtitle1,
+ _ => colors.accentColorDark,
+ };
}
class ShopInBitMessage {
diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
index 1a77816..99e799f 100644
--- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart
+++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
@@ -33,51 +33,6 @@ class ShopInBitTicketDetail extends StatefulWidget {
class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
late final TextEditingController _messageController;
- String _statusLabel(ShopInBitOrderStatus status) {
- switch (status) {
- case ShopInBitOrderStatus.pending:
- return "Pending";
- case ShopInBitOrderStatus.reviewing:
- return "Under review";
- case ShopInBitOrderStatus.offerAvailable:
- return "Offer available";
- case ShopInBitOrderStatus.accepted:
- return "Accepted";
- case ShopInBitOrderStatus.paymentPending:
- return "Awaiting payment";
- case ShopInBitOrderStatus.paid:
- return "Paid";
- case ShopInBitOrderStatus.shipping:
- return "Shipping";
- case ShopInBitOrderStatus.delivered:
- return "Delivered";
- case ShopInBitOrderStatus.closed:
- return "Closed";
- case ShopInBitOrderStatus.cancelled:
- return "Cancelled";
- case ShopInBitOrderStatus.refunded:
- return "Refunded";
- }
- }
-
- Color _statusColor(BuildContext context, ShopInBitOrderStatus status) {
- switch (status) {
- case ShopInBitOrderStatus.delivered:
- return Theme.of(context).extension<StackColors>()!.accentColorGreen;
- case ShopInBitOrderStatus.offerAvailable:
- return Theme.of(context).extension<StackColors>()!.accentColorBlue;
- case ShopInBitOrderStatus.pending:
- case ShopInBitOrderStatus.reviewing:
- return Theme.of(context).extension<StackColors>()!.accentColorYellow;
- case ShopInBitOrderStatus.closed:
- case ShopInBitOrderStatus.cancelled:
- case ShopInBitOrderStatus.refunded:
- return Theme.of(context).extension<StackColors>()!.textSubtitle1;
- default:
- return Theme.of(context).extension<StackColors>()!.accentColorDark;
- }
- }
-
bool _sending = false;
bool _loading = false;
bool _retrying = false;
@@ -425,15 +380,21 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
- color: _statusColor(context, model.status).withOpacity(0.2),
+ color: model.status
+ .getColor(Theme.of(context).extension<StackColors>()!)
+ .withOpacity(0.2),
),
child: Text(
- _statusLabel(model.status),
+ model.status.label,
style:
(isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.itemSubtitle12(context))
- .copyWith(color: _statusColor(context, model.status)),
+ .copyWith(
+ color: model.status.getColor(
+ Theme.of(context).extension<StackColors>()!,
+ ),
+ ),
),
),
],
diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart
index 32b65ce..0ff73b6 100644
--- a/lib/pages/shopinbit/shopinbit_tickets_view.dart
+++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart
@@ -172,51 +172,6 @@ class _ShopInBitTicketsViewState extends State<ShopInBitTicketsView> {
}
}
- String _statusLabel(ShopInBitOrderStatus status) {
- switch (status) {
- case ShopInBitOrderStatus.pending:
- return "Pending";
- case ShopInBitOrderStatus.reviewing:
- return "Under review";
- case ShopInBitOrderStatus.offerAvailable:
- return "Offer available";
- case ShopInBitOrderStatus.accepted:
- return "Accepted";
- case ShopInBitOrderStatus.paymentPending:
- return "Awaiting payment";
- case ShopInBitOrderStatus.paid:
- return "Paid";
- case ShopInBitOrderStatus.shipping:
- return "Shipping";
- case ShopInBitOrderStatus.delivered:
- return "Delivered";
- case ShopInBitOrderStatus.closed:
- return "Closed";
- case ShopInBitOrderStatus.cancelled:
- return "Cancelled";
- case ShopInBitOrderStatus.refunded:
- return "Refunded";
- }
- }
-
- Color _statusColor(BuildContext context, ShopInBitOrderStatus status) {
- switch (status) {
- case ShopInBitOrderStatus.delivered:
- return Theme.of(context).extension<StackColors>()!.accentColorGreen;
- case ShopInBitOrderStatus.offerAvailable:
- return Theme.of(context).extension<StackColors>()!.accentColorBlue;
- case ShopInBitOrderStatus.pending:
- case ShopInBitOrderStatus.reviewing:
- return Theme.of(context).extension<StackColors>()!.accentColorYellow;
- case ShopInBitOrderStatus.closed:
- case ShopInBitOrderStatus.cancelled:
- case ShopInBitOrderStatus.refunded:
- return Theme.of(context).extension<StackColors>()!.textSubtitle1;
- default:
- return Theme.of(context).extension<StackColors>()!.accentColorDark;
- }
- }
-
String _categoryLabel(ShopInBitCategory? category) {
switch (category) {
case ShopInBitCategory.concierge:
@@ -359,13 +314,16 @@ class _ShopInBitTicketsViewState extends State<ShopInBitTicketsView> {
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
- color: _statusColor(
- context,
- ticket.status,
- ).withOpacity(0.2),
+ color: ticket.status
+ .getColor(
+ Theme.of(
+ context,
+ ).extension<StackColors>()!,
+ )
+ .withOpacity(0.2),
),
child: Text(
- _statusLabel(ticket.status),
+ ticket.status.label,
style:
(isDesktop
? STextStyles.desktopTextExtraExtraSmall(
@@ -375,9 +333,10 @@ class _ShopInBitTicketsViewState extends State<ShopInBitTicketsView> {
context,
))
.copyWith(
- color: _statusColor(
- context,
- ticket.status,
+ color: ticket.status.getColor(
+ Theme.of(
+ context,
+ ).extension<StackColors>()!,
),
),
),
diff --git a/lib/services/shopinbit/src/models/ticket.dart b/lib/services/shopinbit/src/models/ticket.dart
index eec6dd3..2f8e91d 100644
--- a/lib/services/shopinbit/src/models/ticket.dart
+++ b/lib/services/shopinbit/src/models/ticket.dart
@@ -16,10 +16,10 @@ enum TicketState {
final String value;
const TicketState(this.value);
- static TicketState fromString(String s) {
+ static TicketState fromString(String value) {
return TicketState.values.firstWhere(
- (e) => e.value == s,
- orElse: () => TicketState.newTicket,
+ (e) => e.value == value,
+ orElse: () => throw Exception("Unknown TicketState string found: $value"),
);
}
}
@@ -104,9 +104,7 @@ class TicketFull {
}
}
-int _toInt(dynamic v) {
- if (v is int) return v;
- if (v is String) return int.parse(v);
- if (v is double) return v.toInt();
- return 0;
+int _toInt(dynamic value) {
+ if (value is int) return value;
+ return int.parse(value.toString());
}
diff --git a/lib/services/shopinbit/src/models/webhook_event.dart b/lib/services/shopinbit/src/models/webhook_event.dart
index 7bf4169..67a160b 100644
--- a/lib/services/shopinbit/src/models/webhook_event.dart
+++ b/lib/services/shopinbit/src/models/webhook_event.dart
@@ -5,10 +5,11 @@ enum WebhookEventType {
final String value;
const WebhookEventType(this.value);
- static WebhookEventType fromString(String s) {
+ static WebhookEventType fromString(String value) {
return WebhookEventType.values.firstWhere(
- (e) => e.value == s,
- orElse: () => WebhookEventType.ticketStateChanged,
+ (e) => e.value == value,
+ orElse: () =>
+ throw Exception("Unknown WebhookEventType string found: $value"),
);
}
}
Why this scored 24/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.