fix(shopinbit): combine by-customer and car-invoice fetches
What changed, and why it matters
This change is a performance fix, not a security fix. It prevents the wallet's ShopInBit ticket screen from making duplicate API calls when several refreshes happen at the same time. The code now shares one in-flight request among callers instead of firing multiple identical requests. There is no evidence in the commit of a vulnerability being patched.
No security action required. Treat as a normal reliability/performance improvement. If reviewing for release notes, classify as a bug fix for redundant network requests in the ShopInBit tickets view.
Security signals we found
No security-relevant code paths modified
Change is purely request deduplication / concurrency control
No mention of vulnerability, CVE, researcher, or security issue in commit message or diff
Evidence from the diff
The commit deduplicates concurrent calls to getTicketsByCustomer and getCurrentCarResearchInvoices by wrapping them in Completers. Previously, overlapping refreshes in the tickets view and post-action refresh paths could each issue their own network request. The patch introduces _ticketsByCustomer and a service-level getCurrentCarResearchInvoices that return the same Future to concurrent callers. No input validation, authentication, cryptography, or parsing logic is changed.
Changed components
lib/services/shopinbit/shopinbit_service.dartlib/pages/shopinbit/shopinbit_tickets_view.dartInspect captured patch +58 / −7
diff --git a/lib/pages/shopinbit/shopinbit_tickets_view.dart b/lib/pages/shopinbit/shopinbit_tickets_view.dart
index 3f941d5..c2a5538 100644
--- a/lib/pages/shopinbit/shopinbit_tickets_view.dart
+++ b/lib/pages/shopinbit/shopinbit_tickets_view.dart
@@ -69,7 +69,6 @@ class _ShopInBitTicketsViewState extends ConsumerState<ShopInBitTicketsView> {
try {
final resp = await ref
.read(pShopinBitService)
- .client
.getCurrentCarResearchInvoices();
final invoices = resp.value;
if (invoices != null) {
diff --git a/lib/services/shopinbit/shopinbit_service.dart b/lib/services/shopinbit/shopinbit_service.dart
index 9f39c8a..b9f7a37 100644
--- a/lib/services/shopinbit/shopinbit_service.dart
+++ b/lib/services/shopinbit/shopinbit_service.dart
@@ -7,6 +7,7 @@ import "../../models/shopinbit/shopinbit_enums.dart";
import "../../utilities/logger.dart";
import "src/api_response.dart";
import "src/client.dart";
+import "src/models/car_research.dart";
import "src/models/message.dart";
import "src/models/ticket.dart";
@@ -21,6 +22,61 @@ class ShopInBitService {
final Map<int, Completer<void>> _inFlight = {};
+ // Combine concurrent list/invoice fetches the same way _refreshRef does, so
+ // overlapping refreshes (e.g. tickets view refresh racing a post-action one)
+ // share a single round-trip instead of each hitting the API.
+ Completer<ApiResponse<List<TicketRef>>>? _ticketsInFlight;
+ String? _ticketsInFlightKey;
+ Completer<ApiResponse<List<CarResearchCurrentInvoice>>>? _carInvoicesInFlight;
+
+ /// Combined by-customer ticket list fetch. Concurrent calls for the same
+ /// key await the same in-flight request.
+ Future<ApiResponse<List<TicketRef>>> _ticketsByCustomer(String key) {
+ final Completer<ApiResponse<List<TicketRef>>>? pending = _ticketsInFlight;
+ if (pending != null && _ticketsInFlightKey == key) {
+ return pending.future;
+ }
+ final Completer<ApiResponse<List<TicketRef>>> completer = Completer();
+ _ticketsInFlight = completer;
+ _ticketsInFlightKey = key;
+ unawaited(
+ client
+ .getTicketsByCustomer(key)
+ .then(completer.complete, onError: completer.completeError)
+ .whenComplete(() {
+ if (_ticketsInFlight == completer) {
+ _ticketsInFlight = null;
+ _ticketsInFlightKey = null;
+ }
+ }),
+ );
+ return completer.future;
+ }
+
+ /// Combined wrapper around the current car research invoices fetch. The
+ /// tickets view calls this on every refresh, so dedup keeps overlapping
+ /// refreshes from each firing their own request.
+ Future<ApiResponse<List<CarResearchCurrentInvoice>>>
+ getCurrentCarResearchInvoices() {
+ final Completer<ApiResponse<List<CarResearchCurrentInvoice>>>? pending =
+ _carInvoicesInFlight;
+ if (pending != null) return pending.future;
+ final Completer<ApiResponse<List<CarResearchCurrentInvoice>>> completer =
+ Completer();
+ _carInvoicesInFlight = completer;
+ unawaited(
+ client
+ .getCurrentCarResearchInvoices()
+ .then(completer.complete, onError: completer.completeError)
+ .whenComplete(() {
+ if (_carInvoicesInFlight == completer) {
+ _carInvoicesInFlight = null;
+ }
+ }),
+ );
+ return completer.future;
+ }
+
// -- Customer key --
/// Returns the most-recently-used customer key. Generates a new one if
@@ -59,9 +115,7 @@ class ShopInBitService {
/// New tickets are hydrated and inserted; existing tickets are patched.
Future<void> refreshAll() async {
final String key = await ensureCustomerKey();
- final ApiResponse<List<TicketRef>> resp = await client.getTicketsByCustomer(
- key,
- );
+ final ApiResponse<List<TicketRef>> resp = await _ticketsByCustomer(key);
if (resp.hasError || resp.value == null) {
Logging.instance.w(
"ShopInBitService.refreshAll: failed to fetch ticket list",
@@ -133,9 +187,7 @@ class ShopInBitService {
/// receipt), hydrate just that one, and return its id; null if not there yet.
Future<int?> adoptRealCarTicket(int receiptTicketId) async {
final String key = await ensureCustomerKey();
- final ApiResponse<List<TicketRef>> resp = await client.getTicketsByCustomer(
- key,
- );
+ final ApiResponse<List<TicketRef>> resp = await _ticketsByCustomer(key);
if (resp.hasError || resp.value == null) return null;
final Set<int> known = (await db.shopInBitTicketsDao.getByCustomerKey(
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.