fix(shopinbit): load offer price in ticket list/detail, add message polling, fix closed ticket messages
What changed, and why it matters
This commit fixes three minor bugs in the Stack Wallet app's ShopInBit ticket feature: it now loads missing offer prices in the ticket list and detail screens, adds automatic 30-second polling for new messages in the detail view, and removes a restriction that previously stopped closed/cancelled/refunded tickets from refreshing their messages. There is no clear security problem in the code changes themselves.
No security action required. Treat as a routine bug-fix commit. If reviewing for reliability, verify that Timer cancellation handles all lifecycle edge cases and that getTicketFull() failures are surfaced to the user appropriately.
Security signals we found
No security-relevant signals detected in the diff
Polling timer is cancelled in widget dispose() to avoid memory leaks
API responses are checked with hasError before use
Evidence from the diff
The patch modifies two Dart files in the ShopInBit UI layer. In the ticket detail screen it adds a periodic Timer that calls _loadFromApi() every 30 seconds unless the ticket is a car-research ticket, and cancels the timer in dispose(). It also fetches the full ticket via getTicketFull() when the status is offerAvailable but the offer product name or price is missing, then updates the local model. In the tickets list view it removes an early-continue that skipped terminal-status tickets and adds the same offer-loading logic plus message refresh for those tickets. The changes are functional/UX fixes; no input validation, authentication, cryptography, or network-trust changes are visible.
Changed components
lib/pages/shopinbit/shopinbit_ticket_detail.dartlib/pages/shopinbit/shopinbit_tickets_view.dartInspect captured patch +33 / −9
diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
index b59239f..85ceb97 100644
--- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart
+++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
@@ -80,6 +80,7 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
bool _sending = false;
bool _loading = false;
bool _retrying = false;
+ Timer? _pollTimer;
@override
void initState() {
@@ -87,11 +88,18 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
_messageController = TextEditingController();
if (widget.model.apiTicketId != 0) {
_loadFromApi();
+ if (!_isCarResearch) {
+ _pollTimer = Timer.periodic(
+ const Duration(seconds: 30),
+ (_) => _loadFromApi(),
+ );
+ }
}
}
@override
void dispose() {
+ _pollTimer?.cancel();
_messageController.dispose();
super.dispose();
}
@@ -130,6 +138,19 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
statusResp.value!.state,
);
}
+
+ if (widget.model.status == ShopInBitOrderStatus.offerAvailable &&
+ (widget.model.offerProductName == null ||
+ widget.model.offerPrice == null)) {
+ final offerResp = await client.getTicketFull(id);
+ if (!offerResp.hasError && offerResp.value != null) {
+ final t = offerResp.value!;
+ widget.model.setOffer(
+ productName: t.productName,
+ price: t.customerPrice,
+ );
+ }
+ }
}
unawaited(
diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart
index 220900a..ce62d3b 100644
--- a/lib/pages/shopinbit/shopinbit_tickets_view.dart
+++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart
@@ -121,15 +121,6 @@ class _ShopInBitTicketsViewState extends State<ShopInBitTicketsView> {
final localIdx = _tickets.indexWhere((t) => t.apiTicketId == ref.id);
if (localIdx < 0) continue;
- // Skip API calls for terminal tickets; they can still be
- // refreshed on-demand when the user opens the detail view.
- final localStatus = _tickets[localIdx].status;
- if (localStatus == ShopInBitOrderStatus.closed ||
- localStatus == ShopInBitOrderStatus.cancelled ||
- localStatus == ShopInBitOrderStatus.refunded) {
- continue;
- }
-
// Car research tickets return 403 on /tickets/:id/* endpoints.
if (_tickets[localIdx].category == ShopInBitCategory.car) continue;
@@ -140,6 +131,18 @@ class _ShopInBitTicketsViewState extends State<ShopInBitTicketsView> {
statusResp.value!.state,
);
+ if (_tickets[localIdx].status == ShopInBitOrderStatus.offerAvailable &&
+ (_tickets[localIdx].offerProductName == null ||
+ _tickets[localIdx].offerPrice == null)) {
+ final offerResp = await service.client.getTicketFull(ref.id);
+ if (!offerResp.hasError && offerResp.value != null) {
+ _tickets[localIdx].setOffer(
+ productName: offerResp.value!.productName,
+ price: offerResp.value!.customerPrice,
+ );
+ }
+ }
+
final msgsResp = await service.client.getMessages(ref.id);
if (!msgsResp.hasError && msgsResp.value != null) {
_tickets[localIdx].clearMessages();
Why this scored 19/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.