Browse Source

feat: Add donation button support (v1.0.2)

- Added url_launcher dependency for opening external donation links
- Implemented donation dialog with Ko-fi, PayPal, and GitHub Sponsors options
- Added 'Support ZenTap' section to settings screen
- Included haptic feedback and error handling for donation links
- Updated version to 1.0.2+3
- Completed Phase 6 step 2: Implement donation button
Fszontagh 5 months ago
parent
commit
f0b9f8e2a3

+ 1 - 1
.roo/rules/ZenTap_Development_Plan.md

@@ -86,7 +86,7 @@
 **Goal**: Add non-intrusive monetization and user customization
 
 - [ ] Integrate optional rewarded ads (e.g. for unlocking themes)
-- [ ] Implement donation button
+- [x] Implement donation button
 - [x] Store user settings locally (theme, sound, haptics)
 
 ---

+ 224 - 1
lib/ui/settings_screen.dart

@@ -1,4 +1,5 @@
 import 'package:flutter/material.dart';
+import 'package:url_launcher/url_launcher.dart';
 import '../utils/colors.dart';
 import '../utils/haptic_utils.dart';
 import '../utils/settings_manager.dart';
@@ -136,13 +137,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
                       
                       const SizedBox(height: 30),
                       
+                      // Support Section
+                      _buildSectionHeader('Support ZenTap'),
+                      _buildDonationTile(),
+                      
+                      const SizedBox(height: 30),
+                      
                       // About Section
                       Padding(
                         padding: const EdgeInsets.only(bottom: 20),
                         child: Column(
                           children: [
                             Text(
-                              'ZenTap v1.0.1',
+                              'ZenTap v1.0.2',
                               style: TextStyle(
                                 color: ZenColors.mutedText,
                                 fontSize: 14,
@@ -364,6 +371,53 @@ class _SettingsScreenState extends State<SettingsScreen> {
     );
   }
 
+  void _showDonationDialog() {
+    if (_hapticsEnabled) {
+      HapticUtils.vibrate(duration: 50);
+    }
+    
+    showDialog(
+      context: context,
+      builder: (context) => _buildDonationDialog(),
+    );
+  }
+
+  Future<void> _openDonationLink(String url) async {
+    if (_hapticsEnabled) {
+      HapticUtils.vibrate(duration: 30);
+    }
+    
+    try {
+      final Uri uri = Uri.parse(url);
+      if (await canLaunchUrl(uri)) {
+        await launchUrl(uri, mode: LaunchMode.externalApplication);
+        Navigator.of(context).pop(); // Close dialog after opening link
+      } else {
+        // Show error if URL can't be launched
+        if (mounted) {
+          ScaffoldMessenger.of(context).showSnackBar(
+            SnackBar(
+              content: const Text('Could not open donation link'),
+              backgroundColor: ZenColors.mutedText,
+              behavior: SnackBarBehavior.floating,
+            ),
+          );
+        }
+      }
+    } catch (e) {
+      // Show error on exception
+      if (mounted) {
+        ScaffoldMessenger.of(context).showSnackBar(
+          SnackBar(
+            content: const Text('Error opening donation link'),
+            backgroundColor: ZenColors.mutedText,
+            behavior: SnackBarBehavior.floating,
+          ),
+        );
+      }
+    }
+  }
+
   Widget _buildTutorialDialog() {
     return AlertDialog(
       backgroundColor: ZenColors.uiElements,
@@ -499,4 +553,173 @@ class _SettingsScreenState extends State<SettingsScreen> {
       ],
     );
   }
+
+  Widget _buildDonationTile() {
+    return _buildActionTile(
+      icon: Icons.favorite,
+      title: 'Support Development',
+      subtitle: 'Help keep ZenTap free and ad-free',
+      onTap: _showDonationDialog,
+    );
+  }
+
+  Widget _buildDonationDialog() {
+    return AlertDialog(
+      backgroundColor: ZenColors.uiElements,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(20),
+      ),
+      title: Row(
+        children: [
+          Icon(
+            Icons.favorite,
+            color: ZenColors.red,
+            size: 24,
+          ),
+          const SizedBox(width: 12),
+          const Text(
+            'Support ZenTap',
+            style: TextStyle(
+              color: ZenColors.primaryText,
+              fontSize: 22,
+              fontWeight: FontWeight.bold,
+            ),
+          ),
+        ],
+      ),
+      content: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(
+            'ZenTap is free and ad-free. If you enjoy using it, consider supporting development:',
+            style: TextStyle(
+              color: ZenColors.secondaryText,
+              fontSize: 14,
+              height: 1.4,
+            ),
+          ),
+          const SizedBox(height: 20),
+          
+          // Ko-fi Button
+          _buildDonationButton(
+            icon: Icons.coffee,
+            title: 'Buy me a coffee',
+            subtitle: 'Ko-fi (one-time)',
+            color: const Color(0xFF13C3FF),
+            onTap: () => _openDonationLink('https://ko-fi.com/fsociety_hu'),
+          ),
+          
+          const SizedBox(height: 12),
+          
+          // PayPal Button
+          _buildDonationButton(
+            icon: Icons.payment,
+            title: 'PayPal Donation',
+            subtitle: 'Direct donation',
+            color: const Color(0xFF0070BA),
+            onTap: () => _openDonationLink('https://paypal.me/fsocietyhu'),
+          ),
+          
+          const SizedBox(height: 12),
+          
+          // GitHub Sponsors Button
+          _buildDonationButton(
+            icon: Icons.code,
+            title: 'GitHub Sponsors',
+            subtitle: 'Monthly support',
+            color: const Color(0xFFEA4AAA),
+            onTap: () => _openDonationLink('https://github.com/sponsors/fszontagh'),
+          ),
+          
+          const SizedBox(height: 16),
+          
+          Text(
+            'Thank you for supporting indie development! 💜',
+            style: TextStyle(
+              color: ZenColors.secondaryText,
+              fontSize: 12,
+              fontStyle: FontStyle.italic,
+            ),
+            textAlign: TextAlign.center,
+          ),
+        ],
+      ),
+      actions: [
+        TextButton(
+          onPressed: () => Navigator.of(context).pop(),
+          child: Text(
+            'Maybe later',
+            style: TextStyle(
+              color: ZenColors.mutedText,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+
+  Widget _buildDonationButton({
+    required IconData icon,
+    required String title,
+    required String subtitle,
+    required Color color,
+    required VoidCallback onTap,
+  }) {
+    return Material(
+      color: color.withValues(alpha: 0.1),
+      borderRadius: BorderRadius.circular(12),
+      child: InkWell(
+        onTap: onTap,
+        borderRadius: BorderRadius.circular(12),
+        child: Container(
+          padding: const EdgeInsets.all(12),
+          decoration: BoxDecoration(
+            borderRadius: BorderRadius.circular(12),
+            border: Border.all(
+              color: color.withValues(alpha: 0.3),
+              width: 1,
+            ),
+          ),
+          child: Row(
+            children: [
+              Icon(
+                icon,
+                color: color,
+                size: 20,
+              ),
+              const SizedBox(width: 12),
+              Expanded(
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Text(
+                      title,
+                      style: TextStyle(
+                        color: ZenColors.primaryText,
+                        fontSize: 14,
+                        fontWeight: FontWeight.w500,
+                      ),
+                    ),
+                    Text(
+                      subtitle,
+                      style: TextStyle(
+                        color: ZenColors.secondaryText,
+                        fontSize: 12,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              Icon(
+                Icons.open_in_new,
+                color: color,
+                size: 16,
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
 }

+ 4 - 0
linux/flutter/generated_plugin_registrant.cc

@@ -7,9 +7,13 @@
 #include "generated_plugin_registrant.h"
 
 #include <audioplayers_linux/audioplayers_linux_plugin.h>
+#include <url_launcher_linux/url_launcher_plugin.h>
 
 void fl_register_plugins(FlPluginRegistry* registry) {
   g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
       fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
   audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
+  g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
+  url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
 }

+ 1 - 0
linux/flutter/generated_plugins.cmake

@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   audioplayers_linux
+  url_launcher_linux
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST

+ 2 - 0
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -10,6 +10,7 @@ import device_info_plus
 import games_services
 import path_provider_foundation
 import shared_preferences_foundation
+import url_launcher_macos
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
@@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   SwiftGamesServicesPlugin.register(with: registry.registrar(forPlugin: "SwiftGamesServicesPlugin"))
   PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+  UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
 }

+ 64 - 0
pubspec.lock

@@ -581,6 +581,70 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.4.0"
+  url_launcher:
+    dependency: "direct main"
+    description:
+      name: url_launcher
+      sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.3.1"
+  url_launcher_android:
+    dependency: transitive
+    description:
+      name: url_launcher_android
+      sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.3.16"
+  url_launcher_ios:
+    dependency: transitive
+    description:
+      name: url_launcher_ios
+      sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
+      url: "https://pub.dev"
+    source: hosted
+    version: "6.3.3"
+  url_launcher_linux:
+    dependency: transitive
+    description:
+      name: url_launcher_linux
+      sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.1"
+  url_launcher_macos:
+    dependency: transitive
+    description:
+      name: url_launcher_macos
+      sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.2.2"
+  url_launcher_platform_interface:
+    dependency: transitive
+    description:
+      name: url_launcher_platform_interface
+      sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.3.2"
+  url_launcher_web:
+    dependency: transitive
+    description:
+      name: url_launcher_web
+      sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.4.1"
+  url_launcher_windows:
+    dependency: transitive
+    description:
+      name: url_launcher_windows
+      sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.1.4"
   uuid:
     dependency: transitive
     description:

+ 4 - 1
pubspec.yaml

@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # In Windows, build-name is used as the major, minor, and patch parts
 # of the product and file versions while build-number is used as the build suffix.
-version: 1.0.1+2
+version: 1.0.2+3
 
 environment:
   sdk: ^3.7.2
@@ -61,6 +61,9 @@ dependencies:
   
   # Google Play Games Services
   games_services: ^4.0.0
+  
+  # URL launcher for donation links
+  url_launcher: ^6.2.2
 
 dev_dependencies:
   flutter_test:

+ 3 - 0
windows/flutter/generated_plugin_registrant.cc

@@ -7,8 +7,11 @@
 #include "generated_plugin_registrant.h"
 
 #include <audioplayers_windows/audioplayers_windows_plugin.h>
+#include <url_launcher_windows/url_launcher_windows.h>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
   AudioplayersWindowsPluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+  UrlLauncherWindowsRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("UrlLauncherWindows"));
 }

+ 1 - 0
windows/flutter/generated_plugins.cmake

@@ -4,6 +4,7 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   audioplayers_windows
+  url_launcher_windows
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST