feat(shopinbit): add warning dialog when closing mid-flow
What changed, and why it matters
This commit adds a confirmation dialog that asks users 'Are you sure you want to close?' when they click the X button partway through a ShopInBit or CakePay purchase flow. Previously, the X button could close the whole checkout window unexpectedly, which might cause a user to lose their place or abandon a payment. There is no sign of a security vulnerability being fixed; it is a usability improvement to prevent accidental closure.
No security action required. Treat as a normal UX/usability improvement. If reviewing for safety, verify that the new confirmation dialog does not block accessibility or emergency cancellation paths.
Security signals we found
No security-relevant code changes (no crypto, network, auth, input validation, or permission changes)
Behavior change is purely UX: adding a confirmation prompt before closing a dialog
No references to vulnerabilities, CVEs, or security reports in commit title or message
Evidence from the diff
The patch introduces a reusable confirmCloseNestedNavigatorDialog() helper and wires it to DesktopDialogCloseButton in several mid-flow views (ShopInBit steps 2-4, ticket detail, and CakePay card/order views). It also exposes the NestedNavigatorDialogState via an inherited widget so child contexts can call close() on the outer dialog. The close button now shows a confirmation dialog before popping the nested navigator dialog, reducing the chance of accidental dismissal during a multi-step flow.
Changed components
lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dartlib/pages/shopinbit/shopinbit_step_2.dartlib/pages/shopinbit/shopinbit_step_3.dartlib/pages/shopinbit/shopinbit_step_4.dartlib/pages/shopinbit/shopinbit_ticket_detail.dartlib/pages/cakepay/cakepay_card_detail_view.dartlib/pages/cakepay/cakepay_order_view.dartInspect captured patch +129 / −21
diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart
index 09a205c..8e1bb84 100644
--- a/lib/pages/cakepay/cakepay_card_detail_view.dart
+++ b/lib/pages/cakepay/cakepay_card_detail_view.dart
@@ -12,6 +12,7 @@ import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart';
import '../../widgets/dialogs/request_external_link_navigation_dialog.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/icon_widgets/credit_card_icon.dart';
@@ -169,7 +170,10 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
style: STextStyles.desktopH3(context),
),
),
- const DesktopDialogCloseButton(),
+ DesktopDialogCloseButton(
+ onPressedOverride: () =>
+ confirmCloseNestedNavigatorDialog(context),
+ ),
],
),
Flexible(
diff --git a/lib/pages/cakepay/cakepay_order_view.dart b/lib/pages/cakepay/cakepay_order_view.dart
index 7488d0e..ba7fb46 100644
--- a/lib/pages/cakepay/cakepay_order_view.dart
+++ b/lib/pages/cakepay/cakepay_order_view.dart
@@ -23,6 +23,7 @@ import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/qr.dart';
import '../../widgets/rounded_white_container.dart';
@@ -865,7 +866,10 @@ class _CakePayOrderViewState extends ConsumerState<CakePayOrderView> {
padding: const EdgeInsets.only(left: 32),
child: Text("Order", style: STextStyles.desktopH3(context)),
),
- const DesktopDialogCloseButton(),
+ DesktopDialogCloseButton(
+ onPressedOverride: () =>
+ confirmCloseNestedNavigatorDialog(context),
+ ),
],
),
Flexible(
diff --git a/lib/pages/shopinbit/shopinbit_step_2.dart b/lib/pages/shopinbit/shopinbit_step_2.dart
index 23403ce..5138a12 100644
--- a/lib/pages/shopinbit/shopinbit_step_2.dart
+++ b/lib/pages/shopinbit/shopinbit_step_2.dart
@@ -13,6 +13,7 @@ import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/rounded_container.dart';
import '../exchange_view/sub_widgets/step_row.dart';
@@ -81,7 +82,10 @@ class _ShopInBitStep2State extends ConsumerState<ShopInBitStep2> {
Text("ShopinBit", style: STextStyles.desktopH3(context)),
],
),
- const DesktopDialogCloseButton(),
+ DesktopDialogCloseButton(
+ onPressedOverride: () =>
+ confirmCloseNestedNavigatorDialog(context),
+ ),
],
),
Flexible(
diff --git a/lib/pages/shopinbit/shopinbit_step_3.dart b/lib/pages/shopinbit/shopinbit_step_3.dart
index f84d487..0b9b9db 100644
--- a/lib/pages/shopinbit/shopinbit_step_3.dart
+++ b/lib/pages/shopinbit/shopinbit_step_3.dart
@@ -11,6 +11,7 @@ import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart';
import '../../widgets/rounded_white_container.dart';
import '../exchange_view/sub_widgets/step_row.dart';
import 'shopinbit_step_4.dart';
@@ -167,7 +168,10 @@ class _ShopInBitStep3State extends ConsumerState<ShopInBitStep3> {
Text("ShopinBit", style: STextStyles.desktopH3(context)),
],
),
- const DesktopDialogCloseButton(),
+ DesktopDialogCloseButton(
+ onPressedOverride: () =>
+ confirmCloseNestedNavigatorDialog(context),
+ ),
],
),
Expanded(
diff --git a/lib/pages/shopinbit/shopinbit_step_4.dart b/lib/pages/shopinbit/shopinbit_step_4.dart
index c5cfd4f..67a67a7 100644
--- a/lib/pages/shopinbit/shopinbit_step_4.dart
+++ b/lib/pages/shopinbit/shopinbit_step_4.dart
@@ -9,6 +9,7 @@ import "../../widgets/conditional_parent.dart";
import "../../widgets/custom_buttons/app_bar_icon_button.dart";
import "../../widgets/desktop/desktop_dialog.dart";
import "../../widgets/desktop/desktop_dialog_close_button.dart";
+import "../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart";
import "step_4_components/shopinbit_car_research_form.dart";
import "step_4_components/shopinbit_concierge_form.dart";
import "step_4_components/shopinbit_generic_form.dart";
@@ -61,7 +62,10 @@ class _ShopInBitStep4DesktopShell extends StatelessWidget {
Text("ShopinBit", style: STextStyles.desktopH3(context)),
],
),
- const DesktopDialogCloseButton(),
+ DesktopDialogCloseButton(
+ onPressedOverride: () =>
+ confirmCloseNestedNavigatorDialog(context),
+ ),
],
),
Expanded(
diff --git a/lib/pages/shopinbit/shopinbit_ticket_detail.dart b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
index be9c71c..59b704a 100644
--- a/lib/pages/shopinbit/shopinbit_ticket_detail.dart
+++ b/lib/pages/shopinbit/shopinbit_ticket_detail.dart
@@ -17,6 +17,7 @@ import '../../widgets/conditional_parent.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
+import '../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/loading_indicator.dart';
import '../../widgets/rounded_container.dart';
@@ -633,7 +634,10 @@ class _ShopInBitTicketDetailState extends ConsumerState<ShopInBitTicketDetail> {
style: STextStyles.desktopH3(context),
),
),
- const DesktopDialogCloseButton(),
+ DesktopDialogCloseButton(
+ onPressedOverride: () =>
+ confirmCloseNestedNavigatorDialog(context),
+ ),
],
),
Expanded(
diff --git a/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart b/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart
index ae54f23..992507e 100644
--- a/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart
+++ b/lib/widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart
@@ -1,5 +1,9 @@
import 'package:flutter/material.dart';
+import '../../../utilities/text_styles.dart';
+import '../../desktop/desktop_dialog.dart';
+import '../../desktop/primary_button.dart';
+import '../../desktop/secondary_button.dart';
import 'nested_navigator_dialog_route_generator.dart';
class NestedNavigatorDialog extends StatefulWidget {
@@ -14,24 +18,39 @@ class NestedNavigatorDialog extends StatefulWidget {
final Object? initialRouteArgs;
final GlobalKey<NavigatorState>? navigatorKey;
+ /// Grabs the nearest [NestedNavigatorDialogState]. Use [maybeOf] if you're
+ /// not sure one exists.
+ static NestedNavigatorDialogState of(BuildContext context) {
+ final NestedNavigatorDialogState? state = maybeOf(context);
+ assert(state != null, "No NestedNavigatorDialog found above this context.");
+ return state!;
+ }
+
+ static NestedNavigatorDialogState? maybeOf(BuildContext context) {
+ return context
+ .dependOnInheritedWidgetOfExactType<_NestedNavigatorDialogScope>()
+ ?.state;
+ }
+
@override
- State<NestedNavigatorDialog> createState() => _NestedNavigatorDialogState();
+ State<NestedNavigatorDialog> createState() => NestedNavigatorDialogState();
}
-class _NestedNavigatorDialogState extends State<NestedNavigatorDialog> {
+class NestedNavigatorDialogState extends State<NestedNavigatorDialog> {
late final _CloseOnEmptyObserver _observer;
late final GlobalKey<NavigatorState> _navigatorKey;
NavigatorState? _parentNavigator;
- void _close() {
+ /// Closes the whole dialog (not just the current step).
+ void close() {
if (mounted) _parentNavigator?.pop();
}
@override
void initState() {
super.initState();
- _observer = _CloseOnEmptyObserver(_close);
+ _observer = _CloseOnEmptyObserver(close);
_navigatorKey = widget.navigatorKey ?? GlobalKey<NavigatorState>();
}
@@ -47,23 +66,40 @@ class _NestedNavigatorDialogState extends State<NestedNavigatorDialog> {
backgroundColor: Colors.transparent,
elevation: 0,
insetPadding: EdgeInsets.zero,
- child: Navigator(
- key: _navigatorKey,
- observers: <NavigatorObserver>[_observer],
- onGenerateRoute: NestedNavigatorDialogRouteGenerator.generateRoute,
- onGenerateInitialRoutes: (_, _) => [
- NestedNavigatorDialogRouteGenerator.generateRoute(
- RouteSettings(
- name: widget.initialRoute,
- arguments: widget.initialRouteArgs,
+ child: _NestedNavigatorDialogScope(
+ state: this,
+ child: Navigator(
+ key: _navigatorKey,
+ observers: <NavigatorObserver>[_observer],
+ onGenerateRoute: NestedNavigatorDialogRouteGenerator.generateRoute,
+ onGenerateInitialRoutes: (_, _) => [
+ NestedNavigatorDialogRouteGenerator.generateRoute(
+ RouteSettings(
+ name: widget.initialRoute,
+ arguments: widget.initialRouteArgs,
+ ),
),
- ),
- ],
+ ],
+ ),
),
);
}
}
+class _NestedNavigatorDialogScope extends InheritedWidget {
+ const _NestedNavigatorDialogScope({
+ required this.state,
+ required super.child,
+ });
+
+ final NestedNavigatorDialogState state;
+
+ @override
+ bool updateShouldNotify(_NestedNavigatorDialogScope oldWidget) {
+ return state != oldWidget.state;
+ }
+}
+
class _CloseOnEmptyObserver extends NavigatorObserver {
_CloseOnEmptyObserver(this.onEmpty);
@@ -74,3 +110,51 @@ class _CloseOnEmptyObserver extends NavigatorObserver {
if (previousRoute == null) onEmpty();
}
}
+
+/// Warns before closing the whole dialog. Wire this to the X on subsequent
+/// (non-root) steps so close does not silently act like back.
+Future<void> confirmCloseNestedNavigatorDialog(BuildContext context) async {
+ final confirmed = await showDialog<bool>(
+ context: context,
+ builder: (ctx) => DesktopDialog(
+ maxWidth: 450,
+ maxHeight: 210,
+ child: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text("Close?", style: STextStyles.desktopH3(ctx)),
+ const SizedBox(height: 12),
+ Text(
+ "Are you sure you want to close?",
+ style: STextStyles.desktopTextMedium(ctx),
+ ),
+ const Spacer(),
+ Row(
+ children: [
+ Expanded(
+ child: SecondaryButton(
+ label: "Cancel",
+ onPressed: () => Navigator.of(ctx).pop(false),
+ ),
+ ),
+ const SizedBox(width: 16),
+ Expanded(
+ child: PrimaryButton(
+ label: "Close",
+ onPressed: () => Navigator.of(ctx).pop(true),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ if (!context.mounted) return;
+ if (confirmed == true) {
+ NestedNavigatorDialog.of(context).close();
+ }
+}
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.