# Files changed · PR \#122 · dara/icarus

[View on GitCafe](https://git.cafe/dara/icarus/pull/122/diffs)

Repository: [dara/icarus](https://git.cafe/dara/icarus)

Visibility: public

[Add sponsors and a one-time support prompt](https://git.cafe/dara/icarus/pull/122.md)

Version: 2

Head commit: 4a7e5e8b8ba0ec8296c3e08dbb70a68abbbb4f7b

Base commit: aeefc69f889d43416cce3f055c93f2b6d3b65d4d

Comparison base: aeefc69f889d43416cce3f055c93f2b6d3b65d4d

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## README.md

Status: modified; +11; −1

```diff
@@ -76,4 +76,14 @@
 
 ## Support
 
+Icarus is completely free and open source. If it helps your team, you can [buy Dara a coffee](https://www.buymeacoffee.com/daradoescode) to support its continued development.
+
-This project is completely free and open source. Your support helps maintain it.
+## Sponsors
+
+<a href="https://www.greptile.com/?utm_source=oss_badge&utm_medium=readme&utm_campaign=greptile_for_open_source">
+  <img src="https://www.greptile.com/badge.svg" alt="Greptile: The War on Bugs" width="600">
+</a>
+
+[Greptile's Open Source Program](https://www.greptile.com/open-source) provides AI code review for the project.
+
+**[OpenAI — Codex for Open Source](https://openai.com/form/codex-for-oss/)** provides tooling and credits for open-source maintenance.
```

## lib/const/hive_boxes.dart

Status: modified; +4; −0

```diff
@@ -5,4 +5,8 @@
   static const appPreferencesBox = "app_preferences_box";
   static const favoriteAgentsBox = "favorite_agents_box";
   static const pinnedItemsBox = "pinned_items_box";
+  static const appFlagsBox = "app_flags_box";
+
+  static const appLaunchCountKey = "app_launch_count";
+  static const supportPromptShownKey = "support_prompt_shown";
 }
```

## lib/const/settings.dart

Status: modified; +8; −1

```diff
@@ -98,6 +98,12 @@
   }
 
   static final Uri dicordLink = Uri.parse("https://discord.gg/PN2uKwCqYB");
+  static final Uri buyMeACoffeeLink =
+      Uri.parse("https://www.buymeacoffee.com/daradoescode");
+  static final Uri greptileOpenSourceLink =
+      Uri.parse("https://www.greptile.com/open-source");
+  static final Uri openAICodexForOssLink =
+      Uri.parse("https://openai.com/form/codex-for-oss/");
 
   static const Duration autoSaveOffset = Duration(seconds: 15);
   static const int versionNumber = 95;
@@ -227,9 +233,10 @@
     required Color backgroundColor,
     String? actionLabel,
     VoidCallback? onActionPressed,
+    Duration autoCloseDuration = const Duration(seconds: 3),
   }) {
     toastification.showCustom(
-      autoCloseDuration: const Duration(seconds: 3),
+      autoCloseDuration: autoCloseDuration,
       alignment: Alignment.bottomCenter,
       builder: (context, holder) {
         final actionIsVisible = actionLabel != null &&
```

## lib/main.dart

Status: modified; +4; −0

```diff
@@ -89,6 +89,10 @@
       await Hive.openBox<AppPreferences>(HiveBoxNames.appPreferencesBox);
       await Hive.openBox<bool>(HiveBoxNames.favoriteAgentsBox);
       await Hive.openBox<int>(HiveBoxNames.pinnedItemsBox);
+      final appFlagsBox = await Hive.openBox<int>(HiveBoxNames.appFlagsBox);
+      final launchCount =
+          appFlagsBox.get(HiveBoxNames.appLaunchCountKey, defaultValue: 0) ?? 0;
+      await appFlagsBox.put(HiveBoxNames.appLaunchCountKey, launchCount + 1);
       await Hive.openBox<dynamic>(AnalyticsService.storageBoxName);
 
       await MapThemeProfilesProvider.bootstrap();
```

## lib/widgets/folder_navigator.dart

Status: modified; +36; −0

```diff
@@ -4,7 +4,9 @@
 import 'package:flutter/foundation.dart' show debugPrint, kDebugMode, kIsWeb;
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:hive_ce/hive.dart';
 import 'package:icarus/const/coordinate_system.dart';
+import 'package:icarus/const/hive_boxes.dart';
 import 'package:icarus/const/routes.dart';
 import 'package:icarus/const/settings.dart';
 import 'package:icarus/const/update_checker.dart';
@@ -25,6 +27,7 @@
 import 'package:icarus/widgets/folder_edit_dialog.dart';
 import 'package:icarus/widgets/ica_drop_target.dart';
 import 'package:shadcn_ui/shadcn_ui.dart';
+import 'package:url_launcher/url_launcher.dart' show launchUrl;
 
 class FolderNavigator extends ConsumerStatefulWidget {
   const FolderNavigator({super.key});
@@ -85,10 +88,43 @@
         _warnWebView();
 
         _warnDemo();
+
+        _maybeShowSupportPrompt();
       }
     });
   }
+
+  Future<void> _maybeShowSupportPrompt() async {
+    if (kIsWeb) return;
+    if (!Hive.isBoxOpen(HiveBoxNames.appFlagsBox)) return;
 
+    final flags = Hive.box<int>(HiveBoxNames.appFlagsBox);
+    final launchCount =
+        flags.get(HiveBoxNames.appLaunchCountKey, defaultValue: 0) ?? 0;
+    final hasShownPrompt =
+        flags.get(HiveBoxNames.supportPromptShownKey, defaultValue: 0) == 1;
+    if (launchCount < 3 || hasShownPrompt) return;
+
+    // Claim the prompt before the delay so a route change or quick exit never
+    // turns a one-time invitation into a recurring interruption.
+    await flags.put(HiveBoxNames.supportPromptShownKey, 1);
+    if (Platform.isWindows && !isWebViewInitialized) return;
+
+    await Future<void>.delayed(const Duration(seconds: 6));
+    if (!mounted) return;
+
+    Settings.showToast(
+      message:
+          'Icarus is free and open source. If it helps your team, you can help keep it going.',
+      backgroundColor: Settings.tacticalVioletTheme.card,
+      actionLabel: 'Buy me a coffee',
+      onActionPressed: () {
+        launchUrl(Settings.buyMeACoffeeLink);
+      },
+      autoCloseDuration: const Duration(seconds: 8),
+    );
+  }
+
   void _warnWebView() async {
     if (kIsWeb) return;
     if (!Platform.isWindows) return;
```

## lib/widgets/settings_tab.dart

Status: modified; +116; −1

```diff
@@ -16,6 +16,7 @@
 import 'package:icarus/widgets/settings_scope_card.dart';
 import 'package:icarus/widgets/text_editing_shortcut_scope.dart';
 import 'package:shadcn_ui/shadcn_ui.dart';
+import 'package:url_launcher/url_launcher.dart' show launchUrl;
 
 enum _SettingsMode {
   strategy,
@@ -31,6 +32,7 @@
   globalMapVisibility,
   globalMapProfiles,
   globalPrivacy,
+  globalSupport,
   shortcuts,
 }
 
@@ -72,7 +74,10 @@
     };
     final scopeValue = switch (_mode) {
       _SettingsMode.strategy => activeStrategyName,
-      _SettingsMode.global => 'Defaults',
+      _SettingsMode.global =>
+        _selectedSection == _SettingsSection.globalSupport
+            ? 'About & sponsors'
+            : 'Defaults',
       _SettingsMode.shortcuts => 'Keybinds',
     };
 
@@ -193,6 +198,7 @@
       case _SettingsSection.globalMapVisibility:
       case _SettingsSection.globalMapProfiles:
       case _SettingsSection.globalPrivacy:
+      case _SettingsSection.globalSupport:
         return _SettingsMode.global;
       case _SettingsSection.shortcuts:
         return _SettingsMode.shortcuts;
@@ -562,6 +568,51 @@
             scope: MapThemeSettingsScope.global,
           ),
         ),
+        const SizedBox(height: 20),
+        const _SectionDivider(),
+        const SizedBox(height: 20),
+        SettingsScopeCard(
+          key: sectionKeys[_SettingsSection.globalSupport],
+          title: "Sponsors & support",
+          description:
+              "Icarus stays free through open-source programs and support from its users.",
+          child: Column(
+            children: [
+              _SupportLinkTile(
+                icon: Icons.bug_report_outlined,
+                title: "Greptile",
+                description:
+                    "AI code review through Greptile's Open Source Program.",
+                actionLabel: "View program",
+                onPressed: () {
+                  launchUrl(Settings.greptileOpenSourceLink);
+                },
+              ),
+              const _SettingsItemDivider(),
+              _SupportLinkTile(
+                icon: Icons.code_outlined,
+                title: "OpenAI",
+                description:
+                    "Tooling and credits through Codex for Open Source.",
+                actionLabel: "View program",
+                onPressed: () {
+                  launchUrl(Settings.openAICodexForOssLink);
+                },
+              ),
+              const _SettingsItemDivider(),
+              _SupportLinkTile(
+                icon: Icons.local_cafe_outlined,
+                title: "Support Icarus",
+                description:
+                    "If Icarus helps your team, you can help keep development going.",
+                actionLabel: "Buy me a coffee",
+                onPressed: () {
+                  launchUrl(Settings.buyMeACoffeeLink);
+                },
+              ),
+            ],
+          ),
+        ),
         const SizedBox(height: 24),
       ],
     );
@@ -1218,6 +1269,12 @@
             isSelected: selectedSection == _SettingsSection.shortcuts,
             onTap: () => onSectionSelected(_SettingsSection.shortcuts),
           ),
+          _SettingsNavItem(
+            icon: Icons.favorite_border_outlined,
+            label: "Sponsors",
+            isSelected: selectedSection == _SettingsSection.globalSupport,
+            onTap: () => onSectionSelected(_SettingsSection.globalSupport),
+          ),
         ],
       ),
     );
@@ -1508,6 +1565,64 @@
   }
 }
 
+class _SupportLinkTile extends StatelessWidget {
+  const _SupportLinkTile({
+    required this.icon,
+    required this.title,
+    required this.description,
+    required this.actionLabel,
+    required this.onPressed,
+  });
+
+  final IconData icon;
+  final String title;
+  final String description;
+  final String actionLabel;
+  final VoidCallback onPressed;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 10),
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.center,
+        children: [
+          _SettingLeadingIcon(
+            icon: icon,
+            accentColor: Settings.tacticalVioletTheme.mutedForeground,
+          ),
+          const SizedBox(width: 10),
+          Expanded(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  title,
+                  style: const TextStyle(fontWeight: FontWeight.w600),
+                ),
+                const SizedBox(height: 2),
+                Text(
+                  description,
+                  style: ShadTheme.of(context).textTheme.small.copyWith(
+                        color: Settings.tacticalVioletTheme.mutedForeground,
+                        height: 1.3,
+                      ),
+                ),
+              ],
+            ),
+          ),
+          const SizedBox(width: 12),
+          ShadButton.secondary(
+            size: ShadButtonSize.sm,
+            onPressed: onPressed,
+            child: Text(actionLabel),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
 class _SettingLeadingIcon extends StatelessWidget {
   const _SettingLeadingIcon({
     required this.icon,
```
