make allow unsigned permanent
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
@@ -25,11 +26,43 @@ import 'package:http/http.dart' as http;
|
|||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
||||||
|
|
||||||
part 'browser_addon.g.dart';
|
part 'browser_addon.g.dart';
|
||||||
|
|
||||||
const _signatureRequiredPref = 'xpinstall.signatures.required';
|
const _signatureRequiredPref = 'xpinstall.signatures.required';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
class AllowUnsignedExtensions extends _$AllowUnsignedExtensions {
|
||||||
|
Future<void> setAllowUnsigned({required bool allow}) async {
|
||||||
|
final fixator = ref.read(preferenceFixatorProvider.notifier);
|
||||||
|
if (allow) {
|
||||||
|
await fixator.register(_signatureRequiredPref, false);
|
||||||
|
} else {
|
||||||
|
await fixator.unregister(_signatureRequiredPref);
|
||||||
|
await GeckoPrefService().applyPrefs({_signatureRequiredPref: true});
|
||||||
|
}
|
||||||
|
state = AsyncData(allow);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<bool> build() async {
|
||||||
|
final prefs =
|
||||||
|
await GeckoPrefService().getPrefs([_signatureRequiredPref]);
|
||||||
|
final pref = prefs[_signatureRequiredPref];
|
||||||
|
final allowUnsigned = pref?.value == false;
|
||||||
|
|
||||||
|
// Re-register with fixator to prevent Gecko from resetting
|
||||||
|
if (allowUnsigned) {
|
||||||
|
await ref
|
||||||
|
.read(preferenceFixatorProvider.notifier)
|
||||||
|
.register(_signatureRequiredPref, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowUnsigned;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class BrowserAddonService extends _$BrowserAddonService {
|
class BrowserAddonService extends _$BrowserAddonService {
|
||||||
Future<Uri> getAddonXpiUrl(String guid) async {
|
Future<Uri> getAddonXpiUrl(String guid) async {
|
||||||
@@ -68,12 +101,7 @@ class BrowserAddonService extends _$BrowserAddonService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> installFromFile(
|
Future<bool> installFromFile(String filePath) async {
|
||||||
String filePath, {
|
|
||||||
bool allowUnsigned = false,
|
|
||||||
}) async {
|
|
||||||
final prefService = GeckoPrefService();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate file exists and has .xpi extension
|
// Validate file exists and has .xpi extension
|
||||||
final file = File(filePath);
|
final file = File(filePath);
|
||||||
@@ -86,25 +114,13 @@ class BrowserAddonService extends _$BrowserAddonService {
|
|||||||
throw Exception('Invalid file type. Expected .xpi file');
|
throw Exception('Invalid file type. Expected .xpi file');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporarily disable signature requirement if user allows unsigned
|
// Create file:// URI and install
|
||||||
if (allowUnsigned) {
|
final fileUri = Uri.file(filePath);
|
||||||
await prefService.applyPrefs({_signatureRequiredPref: false});
|
if (!ref.mounted) return false;
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
await ref.read(addonServiceProvider).installAddon(fileUri);
|
||||||
// Create file:// URI and install
|
|
||||||
final fileUri = Uri.file(filePath);
|
|
||||||
if (!ref.mounted) return false;
|
|
||||||
|
|
||||||
await ref.read(addonServiceProvider).installAddon(fileUri);
|
return true;
|
||||||
|
|
||||||
return true;
|
|
||||||
} finally {
|
|
||||||
// Always restore signature requirement
|
|
||||||
if (allowUnsigned) {
|
|
||||||
await prefService.applyPrefs({_signatureRequiredPref: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
logger.e(
|
logger.e(
|
||||||
'Failed installing from file: $filePath',
|
'Failed installing from file: $filePath',
|
||||||
|
|||||||
@@ -9,6 +9,51 @@ part of 'browser_addon.dart';
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
// ignore_for_file: type=lint, type=warning
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(AllowUnsignedExtensions)
|
||||||
|
final allowUnsignedExtensionsProvider = AllowUnsignedExtensionsProvider._();
|
||||||
|
|
||||||
|
final class AllowUnsignedExtensionsProvider
|
||||||
|
extends $AsyncNotifierProvider<AllowUnsignedExtensions, bool> {
|
||||||
|
AllowUnsignedExtensionsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'allowUnsignedExtensionsProvider',
|
||||||
|
isAutoDispose: false,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$allowUnsignedExtensionsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
AllowUnsignedExtensions create() => AllowUnsignedExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$allowUnsignedExtensionsHash() =>
|
||||||
|
r'3b67f51cabf8e2f37d992320c5b93bb61c6dbe1f';
|
||||||
|
|
||||||
|
abstract class _$AllowUnsignedExtensions extends $AsyncNotifier<bool> {
|
||||||
|
FutureOr<bool> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<AsyncValue<bool>, bool>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<AsyncValue<bool>, bool>,
|
||||||
|
AsyncValue<bool>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ProviderFor(BrowserAddonService)
|
@ProviderFor(BrowserAddonService)
|
||||||
final browserAddonServiceProvider = BrowserAddonServiceProvider._();
|
final browserAddonServiceProvider = BrowserAddonServiceProvider._();
|
||||||
|
|
||||||
@@ -42,7 +87,7 @@ final class BrowserAddonServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$browserAddonServiceHash() =>
|
String _$browserAddonServiceHash() =>
|
||||||
r'1485fd056ce32e142e342e920affb82a5c77a370';
|
r'b90e69ce77b846d6056c800f232a8aa50b4b21d0';
|
||||||
|
|
||||||
abstract class _$BrowserAddonService extends $Notifier<void> {
|
abstract class _$BrowserAddonService extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+4
@@ -23,6 +23,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
|||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
|
import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||||
@@ -286,6 +287,9 @@ class EngineSettingsReplicationService
|
|||||||
.read(preferenceFixatorProvider.notifier)
|
.read(preferenceFixatorProvider.notifier)
|
||||||
.register('intl.accept_languages', settings.locales.join(','));
|
.register('intl.accept_languages', settings.locales.join(','));
|
||||||
|
|
||||||
|
// Initialize unsigned extensions fixator from Gecko pref
|
||||||
|
await ref.read(allowUnsignedExtensionsProvider.future);
|
||||||
|
|
||||||
initialSettingsSent = true;
|
initialSettingsSent = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -44,7 +44,7 @@ final class EngineSettingsReplicationServiceProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$engineSettingsReplicationServiceHash() =>
|
String _$engineSettingsReplicationServiceHash() =>
|
||||||
r'a2fe25ca6458dd22d705affee74036e1ba379972';
|
r'81443c37d18ad82f0694cd0e1115ccdf8a0f4c06';
|
||||||
|
|
||||||
abstract class _$EngineSettingsReplicationService extends $Notifier<void> {
|
abstract class _$EngineSettingsReplicationService extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+2
-53
@@ -40,7 +40,6 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final selectedFile = useState<String?>(null);
|
final selectedFile = useState<String?>(null);
|
||||||
final allowUnsigned = useState(false);
|
|
||||||
final isInstalling = useState(false);
|
final isInstalling = useState(false);
|
||||||
final errorMessage = useState<String?>(null);
|
final errorMessage = useState<String?>(null);
|
||||||
|
|
||||||
@@ -74,10 +73,7 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
|
|||||||
try {
|
try {
|
||||||
await ref
|
await ref
|
||||||
.read(browserAddonServiceProvider.notifier)
|
.read(browserAddonServiceProvider.notifier)
|
||||||
.installFromFile(
|
.installFromFile(selectedFile.value!);
|
||||||
selectedFile.value!,
|
|
||||||
allowUnsigned: allowUnsigned.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showInfoMessage(context, 'Extension installed successfully');
|
showInfoMessage(context, 'Extension installed successfully');
|
||||||
@@ -88,7 +84,7 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
|
|||||||
if (errorString.contains('NotSigned') ||
|
if (errorString.contains('NotSigned') ||
|
||||||
errorString.contains('SIGNEDSTATE')) {
|
errorString.contains('SIGNEDSTATE')) {
|
||||||
errorMessage.value =
|
errorMessage.value =
|
||||||
'This extension is not signed by Mozilla. Enable "Allow unsigned extensions" to install it.';
|
'This extension is not signed by Mozilla. Enable "Allow unsigned extensions" in Extensions settings to install it.';
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = 'Installation failed: $errorString';
|
errorMessage.value = 'Installation failed: $errorString';
|
||||||
}
|
}
|
||||||
@@ -153,53 +149,6 @@ class _InstallLocalAddonSheet extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
SwitchListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: const Text('Allow unsigned extensions'),
|
|
||||||
subtitle: const Text(
|
|
||||||
'Unsigned extensions have not been verified by Mozilla',
|
|
||||||
),
|
|
||||||
value: allowUnsigned.value,
|
|
||||||
onChanged: isInstalling.value
|
|
||||||
? null
|
|
||||||
: (value) => allowUnsigned.value = value,
|
|
||||||
),
|
|
||||||
if (allowUnsigned.value) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.errorContainer.withValues(alpha: 0.5),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.warning_amber,
|
|
||||||
color: Theme.of(context).colorScheme.error,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Only install unsigned extensions from sources you trust. '
|
|
||||||
'They may contain malicious code.',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (errorMessage.value != null) ...[
|
if (errorMessage.value != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
|
|||||||
@@ -17,10 +17,15 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:fading_scroll/fading_scroll.dart';
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_addon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/install_local_addon_dialog.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/install_local_addon_dialog.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart';
|
import 'package:weblibre/features/settings/presentation/widgets/custom_list_tile.dart';
|
||||||
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
|
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
|
||||||
@@ -43,6 +48,8 @@ class ExtensionsSettingsScreen extends StatelessWidget {
|
|||||||
SettingSection(name: 'Extensions'),
|
SettingSection(name: 'Extensions'),
|
||||||
_InstallLocalAddonTile(),
|
_InstallLocalAddonTile(),
|
||||||
_AddonCollectionTile(),
|
_AddonCollectionTile(),
|
||||||
|
SettingSection(name: 'Security'),
|
||||||
|
_AllowUnsignedExtensionsTile(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -105,3 +112,154 @@ class _AddonCollectionTile extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AllowUnsignedExtensionsTile extends ConsumerWidget {
|
||||||
|
const _AllowUnsignedExtensionsTile();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final allowUnsigned = ref.watch(allowUnsignedExtensionsProvider);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
title: const Text('Allow unsigned extensions'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Unsigned extensions have not been verified by Mozilla',
|
||||||
|
),
|
||||||
|
secondary: const Icon(Icons.extension_off),
|
||||||
|
value: allowUnsigned.value ?? false,
|
||||||
|
onChanged: allowUnsigned.isLoading
|
||||||
|
? null
|
||||||
|
: (value) async {
|
||||||
|
if (value) {
|
||||||
|
final confirmed =
|
||||||
|
await _showAllowUnsignedConfirmationDialog(context);
|
||||||
|
if (confirmed != true) return;
|
||||||
|
}
|
||||||
|
await ref
|
||||||
|
.read(allowUnsignedExtensionsProvider.notifier)
|
||||||
|
.setAllowUnsigned(allow: value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (allowUnsigned.value == true)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.errorContainer.withValues(alpha: 0.5),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Theme.of(context).colorScheme.error),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.warning_amber,
|
||||||
|
color: Theme.of(context).colorScheme.error,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Only install unsigned extensions from sources you trust. '
|
||||||
|
'They may contain malicious code.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> _showAllowUnsignedConfirmationDialog(BuildContext context) {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => const _AllowUnsignedConfirmationDialog(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AllowUnsignedConfirmationDialog extends HookWidget {
|
||||||
|
const _AllowUnsignedConfirmationDialog();
|
||||||
|
|
||||||
|
static const _countdownSeconds = 15;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final remaining = useState(_countdownSeconds);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
final timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
|
if (remaining.value > 0) {
|
||||||
|
remaining.value--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return timer.cancel;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final canConfirm = remaining.value == 0;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
title: const Text('Allow unsigned extensions?'),
|
||||||
|
content: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text:
|
||||||
|
"Warning: This significantly weakens your browser's security."
|
||||||
|
'\n\n',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(
|
||||||
|
text:
|
||||||
|
"Unsigned extensions bypass Mozilla's safety review process. "
|
||||||
|
'Malicious extensions can:\n\n'
|
||||||
|
'\u2022 Read and modify everything you see on any website\n'
|
||||||
|
'\u2022 Steal passwords, banking details, and personal data\n'
|
||||||
|
'\u2022 Monitor your browsing activity silently\n'
|
||||||
|
'\u2022 Install additional malware on your device\n\n',
|
||||||
|
),
|
||||||
|
const TextSpan(
|
||||||
|
text:
|
||||||
|
'Only enable this if you are a developer installing your own '
|
||||||
|
'extension or absolutely trust the source.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: canConfirm ? () => Navigator.of(context).pop(true) : null,
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
backgroundColor: theme.colorScheme.error,
|
||||||
|
foregroundColor: theme.colorScheme.onError,
|
||||||
|
),
|
||||||
|
child: Text(canConfirm ? 'Allow' : 'Allow (${remaining.value})'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user