diff --git a/apps/weblibre/lib/core/providers/persisted_bool.dart b/apps/weblibre/lib/core/providers/persisted_bool.dart
index d4fb116a..9087b039 100644
--- a/apps/weblibre/lib/core/providers/persisted_bool.dart
+++ b/apps/weblibre/lib/core/providers/persisted_bool.dart
@@ -33,7 +33,11 @@ enum PersistedBoolKey {
defaultValue: true,
),
infoboxExpanded(key: 'InfoboxExpanded', defaultValue: true),
- tabSuggestions(key: 'TabSuggestions', defaultValue: false);
+ tabSuggestions(key: 'TabSuggestions', defaultValue: false),
+ supporterBannerDismissed(
+ key: 'SupporterBannerDismissed',
+ defaultValue: false,
+ );
const PersistedBoolKey({required this.key, required this.defaultValue});
diff --git a/apps/weblibre/lib/features/account/presentation/widgets/supporter_home_banner.dart b/apps/weblibre/lib/features/account/presentation/widgets/supporter_home_banner.dart
new file mode 100644
index 00000000..4aa1b4c9
--- /dev/null
+++ b/apps/weblibre/lib/features/account/presentation/widgets/supporter_home_banner.dart
@@ -0,0 +1,254 @@
+/*
+ * Copyright (c) 2024-2026 Fabian Freund.
+ *
+ * This file is part of WebLibre
+ * (see https://weblibre.eu).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:weblibre/core/design/app_colors.dart';
+import 'package:weblibre/core/providers/persisted_bool.dart';
+import 'package:weblibre/core/routing/routes.dart';
+import 'package:weblibre/features/account/domain/repositories/account_auth.dart';
+import 'package:weblibre/features/account/domain/repositories/subscription_repository.dart';
+
+/// Promotional banner shown above the quote card on the browser home page,
+/// inviting non-subscribers to become a WebLibre Supporter.
+///
+/// It hides itself when the user already has an active subscription or has
+/// dismissed it. Includes its own bottom spacing so the host layout doesn't
+/// leave a gap when the banner is absent.
+class SupporterHomeBanner extends ConsumerWidget {
+ const SupporterHomeBanner({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final dismissed = ref.watch(
+ persistedBoolProvider(PersistedBoolKey.supporterBannerDismissed),
+ );
+
+ // Only nudge people who aren't already supporting, and only once we have a
+ // definitive answer. The subscription provider reports `inactive` while the
+ // account session is still being restored from the secure store, so gating
+ // on it alone would flash the banner at a subscriber until auth settles.
+ // We therefore wait for auth to resolve first:
+ // - still loading / signing in / error -> stay hidden
+ // - signed out -> definitely no subscription
+ // - signed in -> wait for subscription status
+ final authAsync = ref.watch(accountAuthRepositoryProvider);
+ final subscriptionAsync = ref.watch(subscriptionRepositoryProvider);
+
+ final shouldShow =
+ // In debug builds, bypass the subscriber/auth auto-hide so the banner
+ // is always available to work on regardless of account state.
+ kDebugMode ||
+ switch (authAsync) {
+ AsyncData(value: final auth) when auth.isSignedOut => true,
+ AsyncData(value: final auth) when auth.isSignedIn =>
+ subscriptionAsync.maybeWhen(
+ data: (status) => !status.isActive,
+ orElse: () => false,
+ ),
+ _ => false,
+ };
+
+ if (dismissed || !shouldShow) {
+ return const SizedBox.shrink();
+ }
+
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+
+ void dismiss() {
+ ref
+ .read(
+ persistedBoolProvider(
+ PersistedBoolKey.supporterBannerDismissed,
+ ).notifier,
+ )
+ .set(true);
+ }
+
+ Future openAccount() {
+ return AccountSettingsRoute().push(context);
+ }
+
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 16),
+ child: Container(
+ width: double.infinity,
+ padding: const EdgeInsets.fromLTRB(20, 18, 20, 20),
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.topLeft,
+ end: Alignment.bottomRight,
+ colors: [
+ Color.alphaBlend(
+ AppColors.brandPurple.withValues(alpha: 0.16),
+ colorScheme.surfaceContainerHigh,
+ ),
+ colorScheme.surfaceContainer.withValues(alpha: 0.9),
+ ],
+ ),
+ borderRadius: BorderRadius.circular(28),
+ border: Border.all(
+ color: AppColors.brandPurple.withValues(alpha: 0.45),
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: 36,
+ height: 36,
+ decoration: BoxDecoration(
+ color: AppColors.brandPurple.withValues(alpha: 0.18),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: const Icon(
+ MdiIcons.heartOutline,
+ size: 18,
+ color: AppColors.brandPurple,
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 2),
+ child: Text(
+ 'Support WebLibre, unlock premium features',
+ style: theme.textTheme.titleMedium?.copyWith(
+ fontWeight: FontWeight.w600,
+ height: 1.25,
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ _DismissButton(onPressed: dismiss),
+ ],
+ ),
+ const SizedBox(height: 14),
+ Text(
+ 'Help fund our independent browser by becoming a Supporter and '
+ 'unlock:',
+ style: theme.textTheme.bodyMedium?.copyWith(
+ color: colorScheme.onSurfaceVariant,
+ height: 1.45,
+ ),
+ ),
+ const SizedBox(height: 12),
+ const _FeatureBullet(
+ label: 'Anonymous Premium Search',
+ description:
+ 'Cryptographically blinded tokens — with optional Tor '
+ 'routing — keep your queries unlinkable to your account.',
+ ),
+ const SizedBox(height: 8),
+ const _FeatureBullet(
+ label: 'Encrypted Sync',
+ description:
+ 'Securely sync your settings and preferences across profiles '
+ 'and devices.',
+ ),
+ const SizedBox(height: 18),
+ SizedBox(
+ width: double.infinity,
+ child: FilledButton.icon(
+ onPressed: openAccount,
+ style: FilledButton.styleFrom(
+ backgroundColor: AppColors.brandPurple,
+ foregroundColor: colorScheme.surface,
+ ),
+ icon: const Icon(MdiIcons.heart, size: 18),
+ label: const Text('Become a Supporter'),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _FeatureBullet extends StatelessWidget {
+ final String label;
+ final String description;
+
+ const _FeatureBullet({required this.label, required this.description});
+
+ @override
+ Widget build(BuildContext context) {
+ final theme = Theme.of(context);
+ final colorScheme = theme.colorScheme;
+ final baseStyle = theme.textTheme.bodyMedium?.copyWith(
+ color: colorScheme.onSurfaceVariant,
+ height: 1.4,
+ );
+
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const Padding(
+ padding: EdgeInsets.only(top: 7),
+ child: Icon(Icons.circle, size: 6, color: AppColors.brandPurple),
+ ),
+ const SizedBox(width: 10),
+ Expanded(
+ child: Text.rich(
+ TextSpan(
+ children: [
+ TextSpan(
+ text: '$label: ',
+ style: baseStyle?.copyWith(
+ color: colorScheme.onSurface,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ TextSpan(text: description, style: baseStyle),
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _DismissButton extends StatelessWidget {
+ final VoidCallback onPressed;
+
+ const _DismissButton({required this.onPressed});
+
+ @override
+ Widget build(BuildContext context) {
+ return IconButton(
+ tooltip: 'Dismiss',
+ onPressed: onPressed,
+ visualDensity: VisualDensity.compact,
+ iconSize: 18,
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints.tightFor(width: 32, height: 32),
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ icon: const Icon(Icons.close_rounded),
+ );
+ }
+}
diff --git a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_home.dart b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_home.dart
index 001576c9..48c1ee49 100644
--- a/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_home.dart
+++ b/apps/weblibre/lib/features/geckoview/features/browser/presentation/widgets/browser_home.dart
@@ -23,6 +23,7 @@ import 'package:flutter_svg/svg.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/design/app_colors.dart';
import 'package:weblibre/core/routing/routes.dart';
+import 'package:weblibre/features/account/presentation/widgets/supporter_home_banner.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_list.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/providers/browser_viewport_toolbar_insets.dart';
@@ -141,6 +142,38 @@ class BrowserHome extends ConsumerWidget {
),
const SizedBox(height: 28),
],
+ const SupporterHomeBanner(),
+ Wrap(
+ alignment: WrapAlignment.center,
+ spacing: 12,
+ runSpacing: 12,
+ children: [
+ if (hasTabs)
+ OutlinedButton.icon(
+ onPressed: viewTabs,
+ icon: const Icon(Icons.tab_rounded),
+ label: const Text('View tabs'),
+ ),
+ FilledButton.icon(
+ onPressed: openNewTab,
+ icon: const Icon(Icons.add_rounded),
+ label: const Text('New tab'),
+ ),
+ if (hasContainerTabs)
+ FilledButton.tonalIcon(
+ onPressed: resumeLatestContainerTab,
+ icon: const Icon(Icons.history_rounded),
+ label: const Text('Resume last tab'),
+ )
+ else if (hasTabs && containerData == null)
+ FilledButton.tonalIcon(
+ onPressed: resumeLatestTab,
+ icon: const Icon(Icons.history_rounded),
+ label: const Text('Resume last tab'),
+ ),
+ ],
+ ),
+ const SizedBox(height: 24),
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
@@ -201,37 +234,6 @@ class BrowserHome extends ConsumerWidget {
],
),
),
- const SizedBox(height: 24),
- Wrap(
- alignment: WrapAlignment.center,
- spacing: 12,
- runSpacing: 12,
- children: [
- if (hasTabs)
- OutlinedButton.icon(
- onPressed: viewTabs,
- icon: const Icon(Icons.tab_rounded),
- label: const Text('View tabs'),
- ),
- FilledButton.icon(
- onPressed: openNewTab,
- icon: const Icon(Icons.add_rounded),
- label: const Text('New tab'),
- ),
- if (hasContainerTabs)
- FilledButton.tonalIcon(
- onPressed: resumeLatestContainerTab,
- icon: const Icon(Icons.history_rounded),
- label: const Text('Resume last tab'),
- )
- else if (hasTabs && containerData == null)
- FilledButton.tonalIcon(
- onPressed: resumeLatestTab,
- icon: const Icon(Icons.history_rounded),
- label: const Text('Resume last tab'),
- ),
- ],
- ),
],
),
),