diff --git a/mobile/ios/Runner/NativeConfirmationDialog.swift b/mobile/ios/Runner/NativeConfirmationDialog.swift index f8365b9013e..6a9ecbfb9ff 100644 --- a/mobile/ios/Runner/NativeConfirmationDialog.swift +++ b/mobile/ios/Runner/NativeConfirmationDialog.swift @@ -1,7 +1,7 @@ import Flutter import UIKit -/// Owns a destructive UIKit alert until dismissal and replies exactly once. +/// Owns a UIKit confirmation alert until dismissal and replies exactly once. final class NativeConfirmationDialogCoordinator: NSObject { private let channel: FlutterMethodChannel private weak var parent: UIViewController? @@ -46,7 +46,8 @@ final class NativeConfirmationDialogCoordinator: NSObject { alert.addAction(UIAlertAction(title: cancelLabel, style: .cancel) { [weak self] _ in self?.cancel() }) - alert.addAction(UIAlertAction(title: confirmLabel, style: .destructive) { [weak self] _ in + let destructive = data["destructive"] as? Bool ?? true + alert.addAction(UIAlertAction(title: confirmLabel, style: destructive ? .destructive : .default) { [weak self] _ in self?.finish(true) }) pendingResult = result diff --git a/mobile/lib/features/channels/channel_detail_page.dart b/mobile/lib/features/channels/channel_detail_page.dart index f1990eef053..409fccf003e 100644 --- a/mobile/lib/features/channels/channel_detail_page.dart +++ b/mobile/lib/features/channels/channel_detail_page.dart @@ -6,6 +6,7 @@ import 'bestie_welcome.dart'; import 'bestie_welcome_storage.dart'; import 'agent_reply_handoff.dart'; import 'huddle_timeline.dart'; +import '../../shared/widgets/confirmation_dialog.dart'; import '../../shared/widgets/modal_presentation.dart'; import '../../shared/widgets/ios_scroll_to_top.dart'; import 'message_footer_layout.dart'; diff --git a/mobile/lib/features/channels/channel_detail_page/huddle_sheet.dart b/mobile/lib/features/channels/channel_detail_page/huddle_sheet.dart index 79ddbd9bac4..d5191154c9a 100644 --- a/mobile/lib/features/channels/channel_detail_page/huddle_sheet.dart +++ b/mobile/lib/features/channels/channel_detail_page/huddle_sheet.dart @@ -243,6 +243,15 @@ IosNavigationAction _huddleNavigationAction( ); return; } + // The header button is easy to hit by accident, so a new call asks first. + final confirmed = await showConfirmation( + context: context, + title: 'Start a Huddle?', + message: + 'Your microphone will turn on, and others in this conversation can join.', + confirmLabel: 'Start Huddle', + ); + if (!confirmed || !context.mounted) return; try { await ref .read(mobileHuddleControllerProvider.notifier) diff --git a/mobile/lib/shared/widgets/confirmation_dialog.dart b/mobile/lib/shared/widgets/confirmation_dialog.dart index c41b883c5a2..d613704a2ac 100644 --- a/mobile/lib/shared/widgets/confirmation_dialog.dart +++ b/mobile/lib/shared/widgets/confirmation_dialog.dart @@ -12,6 +12,22 @@ Future showDestructiveConfirmation({ required String title, required String message, required String confirmLabel, +}) => showConfirmation( + context: context, + title: title, + message: message, + confirmLabel: confirmLabel, + destructive: true, +); + +/// Confirms an action using a UIKit alert on iOS. +/// Returns true only when the person explicitly chooses the confirm action. +Future showConfirmation({ + required BuildContext context, + required String title, + required String message, + required String confirmLabel, + bool destructive = false, }) async { if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) { return await const MethodChannel( @@ -21,6 +37,7 @@ Future showDestructiveConfirmation({ 'message': message, 'confirmLabel': confirmLabel, 'cancelLabel': 'Cancel', + 'destructive': destructive, 'dark': context.theme.brightness == Brightness.dark, }) == true; @@ -37,9 +54,11 @@ Future showDestructiveConfirmation({ ), FilledButton( onPressed: () => Navigator.of(dialogContext).pop(true), - style: FilledButton.styleFrom( - backgroundColor: context.colors.error, - ), + style: destructive + ? FilledButton.styleFrom( + backgroundColor: context.colors.error, + ) + : null, child: Text(confirmLabel), ), ], diff --git a/mobile/test/features/channels/channel_detail_page_test.dart b/mobile/test/features/channels/channel_detail_page_test.dart index 9ae89e1d43b..a3c596c3176 100644 --- a/mobile/test/features/channels/channel_detail_page_test.dart +++ b/mobile/test/features/channels/channel_detail_page_test.dart @@ -9529,7 +9529,19 @@ void main() { ); await tester.pumpAndSettle(); + // A stray tap asks first; Cancel leaves no Huddle behind. await tester.tap(find.byKey(const ValueKey('channel-huddle-button'))); + await tester.pumpAndSettle(); + expect(find.text('Start a Huddle?'), findsOneWidget); + await tester.tap(find.widgetWithText(TextButton, 'Cancel')); + await tester.pumpAndSettle(); + expect(find.text('Start a Huddle?'), findsNothing); + expect(relaySession.huddleCreatePublishStarted.isCompleted, isFalse); + + await tester.tap(find.byKey(const ValueKey('channel-huddle-button'))); + await tester.pumpAndSettle(); + await tester.tap(find.widgetWithText(FilledButton, 'Start Huddle')); + await tester.pump(); await relaySession.huddleCreatePublishStarted.future; var transitionCompleted = false; final transition = diff --git a/mobile/test/features/channels/channel_member_profile_actions_test.dart b/mobile/test/features/channels/channel_member_profile_actions_test.dart index 8408b90fa61..1b8cf60bddc 100644 --- a/mobile/test/features/channels/channel_member_profile_actions_test.dart +++ b/mobile/test/features/channels/channel_member_profile_actions_test.dart @@ -200,6 +200,7 @@ void main() { ); expect(calls.single.arguments, containsPair('confirmLabel', 'Remove')); expect(calls.single.arguments, containsPair('cancelLabel', 'Cancel')); + expect(calls.single.arguments, containsPair('destructive', true)); expect(find.byType(AlertDialog), findsNothing); expect(actions.calls, isEmpty); response.complete(confirmed);