refactor(ui): generalized external link launch request dialog
What changed, and why it matters
This commit is a routine user-interface cleanup. It replaces several copies of the same 'Are you sure you want to open this external link?' warning dialog with one shared version. The behavior shown to the user—asking permission before opening a web link in the device's browser—does not change. There is no indication this fixes a security bug.
No security action required. Treat as normal maintenance. If reviewing further, verify the shared dialog still uses `LaunchMode.externalApplication` and still requires explicit user confirmation before launching URLs.
Security signals we found
No change to URL validation or launch mode
No change to user consent flow
No change to network or IPC behavior
Pure code deduplication / refactor
Evidence from the diff
The change refactors duplicated external-link confirmation logic across CakePay, Services, ShopInBit, and desktop ShopInBit pages into a single reusable widget, RequestExternalLinkNavigationDialog, exposed via showRequestExternalLinkAndMaybeLaunch(). It still parses the URL, displays scheme://host, and only calls launchUrl(..., mode: LaunchMode.externalApplication) if the user confirms. Imports for url_launcher and inline dialog code are removed from the call sites. No validation, launch mode, or permission behavior is altered.
Changed components
lib/pages/cakepay/cakepay_card_detail_view.dartlib/pages/more_view/services_view.dartlib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dartlib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dartlib/widgets/dialogs/request_external_link_navigation_dialog.dartInspect captured patch +142 / −270
diff --git a/lib/pages/cakepay/cakepay_card_detail_view.dart b/lib/pages/cakepay/cakepay_card_detail_view.dart
index 53f2ce5..09a205c 100644
--- a/lib/pages/cakepay/cakepay_card_detail_view.dart
+++ b/lib/pages/cakepay/cakepay_card_detail_view.dart
@@ -1,7 +1,6 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
-import 'package:url_launcher/url_launcher.dart';
import '../../services/cakepay/cakepay_service.dart';
import '../../services/cakepay/src/models/card.dart';
@@ -11,10 +10,9 @@ import '../../utilities/util.dart';
import '../../widgets/background.dart';
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/desktop/primary_button.dart';
-import '../../widgets/desktop/secondary_button.dart';
+import '../../widgets/dialogs/request_external_link_navigation_dialog.dart';
import '../../widgets/dialogs/s_dialog.dart';
import '../../widgets/icon_widgets/credit_card_icon.dart';
import '../../widgets/loading_indicator.dart';
@@ -75,101 +73,9 @@ class _CakePayCardDetailViewState extends State<CakePayCardDetailView> {
return true;
}
- Future<bool> _showOpenBrowserWarning(String url) async {
- final uri = Uri.parse(url);
- final shouldContinue = await showDialog<bool>(
- context: context,
- barrierDismissible: false,
- builder: (_) => Util.isDesktop
- ? DesktopDialog(
- maxWidth: 550,
- maxHeight: 250,
- child: Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 32,
- vertical: 20,
- ),
- child: Column(
- children: [
- Text("Attention", style: STextStyles.desktopH2(context)),
- const SizedBox(height: 16),
- Text(
- "You are about to open "
- "${uri.scheme}://${uri.host} "
- "in your browser.",
- style: STextStyles.desktopTextSmall(context),
- ),
- const SizedBox(height: 35),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SecondaryButton(
- width: 200,
- buttonHeight: ButtonHeight.l,
- label: "Cancel",
- onPressed: () {
- Navigator.of(
- context,
- rootNavigator: true,
- ).pop(false);
- },
- ),
- const SizedBox(width: 20),
- PrimaryButton(
- width: 200,
- buttonHeight: ButtonHeight.l,
- label: "Continue",
- onPressed: () {
- Navigator.of(
- context,
- rootNavigator: true,
- ).pop(true);
- },
- ),
- ],
- ),
- ],
- ),
- ),
- )
- : StackDialog(
- title: "Attention",
- message:
- "You are about to open "
- "${uri.scheme}://${uri.host} "
- "in your browser.",
- leftButton: TextButton(
- onPressed: () {
- Navigator.of(context).pop(false);
- },
- child: Text(
- "Cancel",
- style: STextStyles.button(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.accentColorDark,
- ),
- ),
- ),
- rightButton: TextButton(
- style: Theme.of(context)
- .extension<StackColors>()!
- .getPrimaryEnabledButtonStyle(context),
- onPressed: () {
- Navigator.of(context).pop(true);
- },
- child: Text("Continue", style: STextStyles.button(context)),
- ),
- ),
- );
- return shouldContinue ?? false;
- }
-
Future<void> _openTerms() async {
const url = "https://cakepay.com/terms/";
- if (await _showOpenBrowserWarning(url)) {
- await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
- }
+ await showRequestExternalLinkAndMaybeLaunch(context, uri: Uri.parse(url));
}
Future<void> _purchase() async {
diff --git a/lib/pages/more_view/services_view.dart b/lib/pages/more_view/services_view.dart
index b240d60..403ac0b 100644
--- a/lib/pages/more_view/services_view.dart
+++ b/lib/pages/more_view/services_view.dart
@@ -3,7 +3,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
-import 'package:url_launcher/url_launcher.dart';
import '../../models/shopinbit/shopinbit_order_model.dart';
import '../../providers/providers.dart';
@@ -14,6 +13,7 @@ import '../../widgets/background.dart';
import '../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
+import '../../widgets/dialogs/request_external_link_navigation_dialog.dart';
import '../../widgets/rounded_white_container.dart';
import '../../widgets/stack_dialog.dart';
import '../shopinbit/shopinbit_settings_view.dart';
@@ -31,44 +31,6 @@ class ServicesView extends ConsumerStatefulWidget {
}
class _ServicesViewState extends ConsumerState<ServicesView> {
- Future<bool> _showOpenBrowserWarning(BuildContext context, String url) async {
- final uri = Uri.parse(url);
- final shouldContinue = await showDialog<bool>(
- context: context,
- barrierDismissible: false,
- builder: (_) => StackDialog(
- title: "Attention",
- message:
- "You are about to open "
- "${uri.scheme}://${uri.host} "
- "in your browser.",
- leftButton: TextButton(
- onPressed: () {
- Navigator.of(context).pop(false);
- },
- child: Text(
- "Cancel",
- style: STextStyles.button(context).copyWith(
- color: Theme.of(
- context,
- ).extension<StackColors>()!.accentColorDark,
- ),
- ),
- ),
- rightButton: TextButton(
- style: Theme.of(
- context,
- ).extension<StackColors>()!.getPrimaryEnabledButtonStyle(context),
- onPressed: () {
- Navigator.of(context).pop(true);
- },
- child: Text("Continue", style: STextStyles.button(context)),
- ),
- ),
- );
- return shouldContinue ?? false;
- }
-
void _showShopDialog() {
showDialog<void>(
context: context,
@@ -99,16 +61,11 @@ class _ServicesViewState extends ConsumerState<ServicesView> {
..onTap = () async {
const url =
"https://api.shopinbit.com/static/policy/privacy.html";
- final shouldOpen = await _showOpenBrowserWarning(
- dialogContext,
- url,
+
+ await showRequestExternalLinkAndMaybeLaunch(
+ context,
+ uri: Uri.parse(url),
);
- if (shouldOpen) {
- await launchUrl(
- Uri.parse(url),
- mode: LaunchMode.externalApplication,
- );
- }
},
),
const TextSpan(text: "."),
@@ -270,14 +227,11 @@ class _ServicesViewState extends ConsumerState<ServicesView> {
..onTap = () async {
const url =
"https://api.shopinbit.com/static/policy/terms.html";
- final shouldOpen =
- await _showOpenBrowserWarning(context, url);
- if (shouldOpen) {
- await launchUrl(
- Uri.parse(url),
- mode: LaunchMode.externalApplication,
- );
- }
+
+ await showRequestExternalLinkAndMaybeLaunch(
+ context,
+ uri: Uri.parse(url),
+ );
},
),
const TextSpan(text: " and "),
@@ -290,14 +244,11 @@ class _ServicesViewState extends ConsumerState<ServicesView> {
..onTap = () async {
const url =
"https://api.shopinbit.com/static/policy/privacy.html";
- final shouldOpen =
- await _showOpenBrowserWarning(context, url);
- if (shouldOpen) {
- await launchUrl(
- Uri.parse(url),
- mode: LaunchMode.externalApplication,
- );
- }
+
+ await showRequestExternalLinkAndMaybeLaunch(
+ context,
+ uri: Uri.parse(url),
+ );
},
),
const TextSpan(text: "."),
diff --git a/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart b/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart
index 72d9505..d5d3347 100644
--- a/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart
+++ b/lib/pages/shopinbit/step_4_components/shopinbit_privacy_checkbox.dart
@@ -1,13 +1,12 @@
import "package:flutter/gestures.dart";
import "package:flutter/material.dart";
-import "package:url_launcher/url_launcher.dart";
import "../../../utilities/text_styles.dart";
import "../../../utilities/util.dart";
import "../../../widgets/desktop/desktop_dialog.dart";
import "../../../widgets/desktop/primary_button.dart";
import "../../../widgets/desktop/secondary_button.dart";
-import "../../../widgets/stack_dialog.dart";
+import "../../../widgets/dialogs/request_external_link_navigation_dialog.dart";
const String _shopInBitPrivacyUrl =
"https://api.shopinbit.com/static/policy/privacy.html";
@@ -22,45 +21,6 @@ class ShopInBitPrivacyCheckbox extends StatelessWidget {
final bool value;
final ValueChanged<bool> onChanged;
- Future<void> _openPrivacyPolicy(BuildContext context) async {
- final bool shouldOpen = await _showOpenBrowserWarning(
- context,
- _shopInBitPrivacyUrl,
- );
- if (shouldOpen) {
- await launchUrl(
- Uri.parse(_shopInBitPrivacyUrl),
- mode: LaunchMode.externalApplication,
- );
- }
- }
-
- Future<bool> _showOpenBrowserWarning(BuildContext context, String url) async {
- final Uri uri = Uri.parse(url);
- final String message =
- "You are about to open ${uri.scheme}://${uri.host} in your browser.";
-
- final bool? shouldContinue = await showDialog<bool>(
- context: context,
- barrierDismissible: false,
- builder: (context) => Util.isDesktop
- ? _DesktopBrowserWarning(message: message)
- : StackDialog(
- title: "Attention",
- message: message,
- leftButton: SecondaryButton(
- label: "Cancel",
- onPressed: () => Navigator.of(context).pop(false),
- ),
- rightButton: PrimaryButton(
- label: "Continue",
- onPressed: () => Navigator.of(context).pop(true),
- ),
- ),
- );
- return shouldContinue ?? false;
- }
-
@override
Widget build(BuildContext context) {
final isDesktop = Util.isDesktop;
@@ -105,7 +65,10 @@ class ShopInBitPrivacyCheckbox extends StatelessWidget {
context,
).copyWith(fontSize: isDesktop ? 18 : 14),
recognizer: TapGestureRecognizer()
- ..onTap = () => _openPrivacyPolicy(context),
+ ..onTap = () => showRequestExternalLinkAndMaybeLaunch(
+ context,
+ uri: Uri.parse(_shopInBitPrivacyUrl),
+ ),
),
const TextSpan(text: "."),
],
diff --git a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart
index 85d1a19..302794c 100644
--- a/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart
+++ b/lib/pages_desktop_specific/services/shopin_bit/desktop_shopinbit_view.dart
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
-import 'package:url_launcher/url_launcher.dart';
import '../../../app_config.dart';
import '../../../models/shopinbit/shopinbit_order_model.dart';
@@ -22,6 +21,7 @@ import '../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/desktop/secondary_button.dart';
import '../../../widgets/dialogs/nested_navigator_dialog/nested_navigator_dialog.dart';
+import '../../../widgets/dialogs/request_external_link_navigation_dialog.dart';
import '../../../widgets/rounded_container.dart';
import '../../../widgets/rounded_white_container.dart';
import '../../../widgets/textfields/adaptive_text_field.dart';
@@ -40,57 +40,6 @@ class DesktopShopInBitView extends ConsumerStatefulWidget {
}
class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
- Future<bool> _showOpenBrowserWarning(BuildContext context, String url) async {
- final uri = Uri.parse(url);
- final shouldContinue = await showDialog<bool>(
- context: context,
- barrierDismissible: false,
- builder: (_) => DesktopDialog(
- maxWidth: 550,
- maxHeight: 250,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20),
- child: Column(
- children: [
- Text("Attention", style: STextStyles.desktopH2(context)),
- const SizedBox(height: 16),
- Text(
- "You are about to open "
- "${uri.scheme}://${uri.host} "
- "in your browser.",
- style: STextStyles.desktopTextSmall(context),
- ),
- const SizedBox(height: 35),
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- SecondaryButton(
- width: 200,
- buttonHeight: ButtonHeight.l,
- label: "Cancel",
- onPressed: () {
- Navigator.of(context, rootNavigator: true).pop(false);
- },
- ),
- const SizedBox(width: 20),
- PrimaryButton(
- width: 200,
- buttonHeight: ButtonHeight.l,
- label: "Continue",
- onPressed: () {
- Navigator.of(context, rootNavigator: true).pop(true);
- },
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- return shouldContinue ?? false;
- }
-
Future<void> _showShopDialog() async {
final dao = ref.read(pSharedDrift).shopinBitSettingsDao;
final settings = await dao.getSettings();
@@ -196,16 +145,10 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
..onTap = () async {
const url =
"https://api.shopinbit.com/static/policy/terms.html";
- final shouldOpen = await _showOpenBrowserWarning(
+ await showRequestExternalLinkAndMaybeLaunch(
context,
- url,
+ uri: Uri.parse(url),
);
- if (shouldOpen) {
- await launchUrl(
- Uri.parse(url),
- mode: LaunchMode.externalApplication,
- );
- }
},
),
const TextSpan(text: " and "),
@@ -218,16 +161,11 @@ class _DesktopServicesViewState extends ConsumerState<DesktopShopInBitView> {
..onTap = () async {
const url =
"https://api.shopinbit.com/static/policy/privacy.html";
- final shouldOpen = await _showOpenBrowserWarning(
+
+ await showRequestExternalLinkAndMaybeLaunch(
context,
- url,
+ uri: Uri.parse(url),
);
- if (shouldOpen) {
- await launchUrl(
- Uri.parse(url),
- mode: LaunchMode.externalApplication,
- );
- }
},
),
const TextSpan(text: "."),
diff --git a/lib/widgets/dialogs/request_external_link_navigation_dialog.dart b/lib/widgets/dialogs/request_external_link_navigation_dialog.dart
new file mode 100644
index 0000000..a5491a5
--- /dev/null
+++ b/lib/widgets/dialogs/request_external_link_navigation_dialog.dart
@@ -0,0 +1,114 @@
+import 'package:flutter/material.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+import '../../utilities/text_styles.dart';
+import '../../utilities/util.dart';
+import '../conditional_parent.dart';
+import '../desktop/desktop_dialog_close_button.dart';
+import '../desktop/primary_button.dart';
+import '../desktop/secondary_button.dart';
+import 's_dialog.dart';
+
+Future<void> showRequestExternalLinkAndMaybeLaunch(
+ BuildContext context, {
+ required Uri uri,
+}) async {
+ final shouldContinue = await showDialog<bool>(
+ context: context,
+ barrierDismissible: false,
+ builder: (context) => RequestExternalLinkNavigationDialog(uri: uri),
+ );
+
+ if (shouldContinue == true) {
+ await launchUrl(uri, mode: LaunchMode.externalApplication);
+ }
+}
+
+class RequestExternalLinkNavigationDialog extends StatefulWidget {
+ const RequestExternalLinkNavigationDialog({super.key, required this.uri});
+
+ final Uri uri;
+
+ @override
+ State<RequestExternalLinkNavigationDialog> createState() =>
+ _RequestExternalLinkNavigationDialogState();
+}
+
+class _RequestExternalLinkNavigationDialogState
+ extends State<RequestExternalLinkNavigationDialog> {
+ @override
+ Widget build(BuildContext context) {
+ return SDialog(
+ child: ConditionalParent(
+ condition: Util.isDesktop,
+ builder: (child) => SizedBox(width: 500, child: child),
+ child: Column(
+ mainAxisSize: .min,
+ crossAxisAlignment: .start,
+ children: [
+ Padding(
+ padding: .only(
+ left: Util.isDesktop ? 32 : 16,
+ top: Util.isDesktop ? 0 : 16,
+ bottom: Util.isDesktop ? 16 : 8,
+ ),
+ child: Row(
+ mainAxisAlignment: .spaceBetween,
+ children: [
+ SelectableText(
+ "Attention",
+ style: Util.isDesktop
+ ? STextStyles.desktopH3(context)
+ : STextStyles.pageTitleH2(context),
+ ),
+ if (Util.isDesktop) const DesktopDialogCloseButton(),
+ ],
+ ),
+ ),
+ Padding(
+ padding: .symmetric(horizontal: Util.isDesktop ? 32 : 16),
+ child: Text(
+ "You are about to open "
+ "${widget.uri.scheme}://${widget.uri.host} "
+ "in your browser.",
+ style: Util.isDesktop
+ ? STextStyles.desktopTextSmall(context)
+ : STextStyles.smallMed14(context),
+ ),
+ ),
+ Padding(
+ padding: .only(
+ top: Util.isDesktop ? 32 : 24,
+ left: Util.isDesktop ? 32 : 16,
+ right: Util.isDesktop ? 32 : 16,
+ bottom: Util.isDesktop ? 32 : 16,
+ ),
+ child: Row(
+ mainAxisAlignment: .spaceBetween,
+ children: [
+ Expanded(
+ child: SecondaryButton(
+ label: "Cancel",
+ buttonHeight: Util.isDesktop ? .l : null,
+ onPressed: Navigator.of(context).pop,
+ ),
+ ),
+ Util.isDesktop
+ ? const SizedBox(width: 32)
+ : const SizedBox(width: 16),
+ Expanded(
+ child: PrimaryButton(
+ label: "Continue",
+ buttonHeight: Util.isDesktop ? .l : null,
+ onPressed: () => Navigator.of(context).pop(true),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
Why this scored 17/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.