feat(shopinbit): add Complete Request retry button to ticket detail
What changed, and why it matters
This commit adds a 'Complete Request' retry button to a ticket detail screen for a car-research ordering feature. It lets users resubmit a request that previously failed to create. The change is a routine UI/flow improvement and does not, on its own, introduce an obvious security vulnerability. There is no indication in the commit that this fixes a security issue or that it was disclosed as such.
No immediate security action required. As a defensive review, verify that `ShopInBitService.instance.client.createRequest()` and the backend enforce idempotency or duplicate-prevention for retried car-research requests, and confirm that `model.requestDescription` is appropriately sanitized server-side since the diff only replays it.
Security signals we found
No security-relevant keywords in commit title or message
No changes to cryptography, authentication, authorization, or network trust boundaries
New network call reuses existing authenticated client (`ShopInBitService.instance.client`)
User-provided model fields (`displayName`, `requestDescription`, `deliveryCountry`) are replayed to backend without additional sanitization visible in this diff
No explicit rate limiting or duplicate-submission guard beyond a local `_retrying` boolean
Evidence from the diff
The patch adds a _retryCreateRequest() method in shopinbit_ticket_detail.dart that re-calls ShopInBitService.instance.client.createRequest() using fields from the existing widget.model, persists the new ticket reference to Isar via MainDB.instance.putShopInBitTicket(), clears model.needsCreateRequest, and shows success/warning flush bars. A new _retrying state flag and a conditional ‘Complete Request’ button are added to the widget build. No input validation, authentication, or authorization logic is changed; the method reuses existing service and database abstractions.
Changed components
lib/pages/shopinbit/shopinbit_ticket_detail.dartShopInBit car research order flowShopInBitService client createRequest endpointInspect captured patch +92 / −1
diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
index 76299d1..b59239f 100644
--- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart
+++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import '../../db/isar/main_db.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
+import '../../notifications/show_flush_bar.dart';
import '../../services/shopinbit/shopinbit_service.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/text_styles.dart';
@@ -78,6 +79,7 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
bool _sending = false;
bool _loading = false;
+ bool _retrying = false;
@override
void initState() {
@@ -172,6 +174,78 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
}
}
+ Future<void> _retryCreateRequest() async {
+ if (_retrying) return;
+ setState(() => _retrying = true);
+
+ try {
+ final model = widget.model;
+ final customerKey = await ShopInBitService.instance.ensureCustomerKey();
+ final comment =
+ "${model.requestDescription}\n\n"
+ "The Client paid the car research fee (#${model.feeTicketNumber})";
+
+ final reqResp = await ShopInBitService.instance.client.createRequest(
+ customerPseudonym: model.displayName,
+ externalCustomerKey: customerKey,
+ serviceType: "car_research",
+ comment: comment,
+ deliveryCountry: model.deliveryCountry,
+ );
+
+ if (reqResp.hasError || reqResp.value == null) {
+ if (mounted) {
+ setState(() => _retrying = false);
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message: reqResp.exception?.message ?? "Failed to create request",
+ context: context,
+ ),
+ );
+ }
+ return;
+ }
+
+ final requestRef = reqResp.value!;
+ final requestModel = ShopInBitOrderModel()
+ ..ticketId = requestRef.number
+ ..apiTicketId = requestRef.id
+ ..category = ShopInBitCategory.car
+ ..status = ShopInBitOrderStatus.pending
+ ..displayName = model.displayName
+ ..requestDescription = model.requestDescription
+ ..deliveryCountry = model.deliveryCountry;
+ await MainDB.instance.putShopInBitTicket(requestModel.toIsarTicket());
+
+ model.needsCreateRequest = false;
+ await MainDB.instance.putShopInBitTicket(model.toIsarTicket());
+
+ if (!mounted) return;
+ setState(() => _retrying = false);
+
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.success,
+ message: "Car research request submitted successfully!",
+ context: context,
+ ),
+ );
+ Navigator.of(context).pop();
+ } catch (e) {
+ if (mounted) {
+ setState(() => _retrying = false);
+ unawaited(
+ showFloatingFlushBar(
+ type: FlushBarType.warning,
+ message: e.toString(),
+ context: context,
+ ),
+ );
+ }
+ }
+ }
+
String _formatTime(DateTime dt) {
final hour = dt.hour.toString().padLeft(2, '0');
final minute = dt.minute.toString().padLeft(2, '0');
@@ -461,7 +535,8 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
),
);
- final requestDetailsSection = _isCarResearch && model.requestDescription.isNotEmpty
+ final requestDetailsSection =
+ _isCarResearch && model.requestDescription.isNotEmpty
? Padding(
padding: EdgeInsets.only(bottom: isDesktop ? 12 : 8),
child: RoundedWhiteContainer(
@@ -487,9 +562,25 @@ class _ShopInBitTicketDetailState extends State<ShopInBitTicketDetail> {
)
: const SizedBox.shrink();
+ final retryButton =
+ widget.model.needsCreateRequest &&
+ widget.model.category == ShopInBitCategory.car
+ ? Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ child: PrimaryButton(
+ label: _retrying ? "Submitting..." : "Complete Request",
+ enabled: !_retrying,
+ onPressed: _retrying
+ ? null
+ : () => unawaited(_retryCreateRequest()),
+ ),
+ )
+ : const SizedBox.shrink();
+
final body = Column(
children: [
statusBar,
+ retryButton,
offerBanner,
requestDetailsSection,
chatArea,
Why this scored 20/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.