using BuyMeCofee.Maui.Constants; using BuyMeCofee.Maui.Helpers; using BuyMeCofee.Maui.Models; using Microsoft.Maui.Controls.Shapes; namespace BuyMeCofee.Maui.Controls; /// /// A branded Buy Me a Coffee support widget with amount entry, preset chips, /// name/message fields, and a monthly toggle. Opens the BMC page on support tap. /// public class BuyMeACoffeeWidget : ContentView { #region Bindable Properties public static readonly BindableProperty UsernameProperty = BindableProperty.Create(nameof(Username), typeof(string), typeof(BuyMeACoffeeWidget), string.Empty, propertyChanged: OnUsernameChanged); public static readonly BindableProperty DisplayNameProperty = BindableProperty.Create(nameof(DisplayName), typeof(string), typeof(BuyMeACoffeeWidget), string.Empty, propertyChanged: OnDisplayNameChanged); public static readonly BindableProperty SuggestedAmountsProperty = BindableProperty.Create(nameof(SuggestedAmounts), typeof(int[]), typeof(BuyMeACoffeeWidget), BmcConstants.DefaultSuggestedAmounts, propertyChanged: OnChipsChanged); public static readonly BindableProperty DefaultAmountProperty = BindableProperty.Create(nameof(DefaultAmount), typeof(int), typeof(BuyMeACoffeeWidget), 5, propertyChanged: OnDefaultAmountChanged); public static readonly BindableProperty AccentColorProperty = BindableProperty.Create(nameof(AccentColor), typeof(Color), typeof(BuyMeACoffeeWidget), Color.FromArgb(BmcColors.WidgetPurple), propertyChanged: OnAccentChanged); public static readonly BindableProperty ShowMonthlyOptionProperty = BindableProperty.Create(nameof(ShowMonthlyOption), typeof(bool), typeof(BuyMeACoffeeWidget), true, propertyChanged: OnShowMonthlyChanged); public static readonly BindableProperty SupportButtonTextProperty = BindableProperty.Create(nameof(SupportButtonText), typeof(string), typeof(BuyMeACoffeeWidget), BmcConstants.DefaultSupportButtonText); #endregion #region Properties /// Your Buy Me a Coffee username/slug. public string Username { get => (string)GetValue(UsernameProperty); set => SetValue(UsernameProperty, value); } /// Name displayed in "Support {DisplayName}" header. public string DisplayName { get => (string)GetValue(DisplayNameProperty); set => SetValue(DisplayNameProperty, value); } /// Preset amount chips. Default: [25, 50, 100] public int[] SuggestedAmounts { get => (int[])GetValue(SuggestedAmountsProperty); set => SetValue(SuggestedAmountsProperty, value); } /// Initial amount in the entry field. Default: 5 public int DefaultAmount { get => (int)GetValue(DefaultAmountProperty); set => SetValue(DefaultAmountProperty, value); } /// Accent color for the Support button. Default: BMC purple (#6C5CE7) public Color AccentColor { get => (Color)GetValue(AccentColorProperty); set => SetValue(AccentColorProperty, value); } /// Whether to show the "Make this monthly" checkbox. Default: true public bool ShowMonthlyOption { get => (bool)GetValue(ShowMonthlyOptionProperty); set => SetValue(ShowMonthlyOptionProperty, value); } /// Text on the support button. Default: "Support" public string SupportButtonText { get => (string)GetValue(SupportButtonTextProperty); set => SetValue(SupportButtonTextProperty, value); } #endregion /// Raised when the user taps the Support button, before opening the browser. public event EventHandler? SupportRequested; // Internal UI elements private Label _headerLabel = null!; private Entry _amountEntry = null!; private HorizontalStackLayout _chipsRow = null!; private Entry _nameEntry = null!; private Editor _messageEditor = null!; private CheckBox _monthlyCheckBox = null!; private HorizontalStackLayout _monthlyRow = null!; private Border _supportButton = null!; private Label _supportButtonLabel = null!; private Label _footerLabel = null!; private int _currentAmount; public BuyMeACoffeeWidget() { _currentAmount = DefaultAmount; BuildLayout(); } private void BuildLayout() { // Header _headerLabel = new Label { Text = string.IsNullOrWhiteSpace(DisplayName) ? "Support" : $"Support {DisplayName}", FontSize = 20, FontAttributes = FontAttributes.Bold, Margin = new Thickness(0, 0, 0, 8), }; // Amount row: $ + Entry + chips var dollarLabel = new Label { Text = "$", FontSize = 18, VerticalOptions = LayoutOptions.Center, TextColor = Colors.Gray, }; _amountEntry = new Entry { Text = _currentAmount.ToString(), Keyboard = Keyboard.Numeric, FontSize = 16, Placeholder = "Enter amount", HorizontalOptions = LayoutOptions.Fill, VerticalOptions = LayoutOptions.Center, }; _amountEntry.TextChanged += OnAmountTextChanged; _chipsRow = new HorizontalStackLayout { Spacing = 6 }; BuildChips(); var amountRow = new Grid { ColumnDefinitions = { new ColumnDefinition(GridLength.Auto), new ColumnDefinition(GridLength.Star), new ColumnDefinition(GridLength.Auto), }, ColumnSpacing = 8, VerticalOptions = LayoutOptions.Center, }; amountRow.Add(dollarLabel, 0); amountRow.Add(_amountEntry, 1); amountRow.Add(_chipsRow, 2); var amountBorder = CreateFieldBorder(amountRow); // Name field _nameEntry = new Entry { Placeholder = "Name or @yoursocial", FontSize = 14, }; var nameBorder = CreateFieldBorder(_nameEntry); // Message field _messageEditor = new Editor { Placeholder = "Say something nice...", FontSize = 14, HeightRequest = 80, AutoSize = EditorAutoSizeOption.TextChanges, }; var messageBorder = CreateFieldBorder(_messageEditor); // Monthly toggle _monthlyCheckBox = new CheckBox { VerticalOptions = LayoutOptions.Center, }; var monthlyLabel = new Label { Text = "Make this monthly", FontSize = 14, VerticalOptions = LayoutOptions.Center, }; _monthlyRow = new HorizontalStackLayout { Spacing = 4, IsVisible = ShowMonthlyOption, Children = { _monthlyCheckBox, monthlyLabel } }; // Support button _supportButtonLabel = new Label { Text = SupportButtonText, FontSize = 16, FontAttributes = FontAttributes.Bold, TextColor = Colors.White, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, }; _supportButton = new Border { BackgroundColor = AccentColor, StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(24) }, Stroke = Colors.Transparent, Padding = new Thickness(0, 14), HorizontalOptions = LayoutOptions.Fill, Content = _supportButtonLabel, }; var supportTap = new TapGestureRecognizer(); supportTap.Tapped += OnSupportTapped; _supportButton.GestureRecognizers.Add(supportTap); var supportHover = new PointerGestureRecognizer(); supportHover.PointerEntered += (_, _) => _supportButton.Opacity = 0.85; supportHover.PointerExited += (_, _) => _supportButton.Opacity = 1.0; _supportButton.GestureRecognizers.Add(supportHover); // Redirect note var redirectNote = new Label { Text = "You'll complete your support on buymeacoffee.com", FontSize = 11, TextColor = Colors.Gray, HorizontalOptions = LayoutOptions.Center, Margin = new Thickness(0, -4, 0, 0), }; // Footer var footerCup = new Image { Source = BmcBrandAssets.GetCupLogo(), HeightRequest = 16, Aspect = Aspect.AspectFit, VerticalOptions = LayoutOptions.Center, }; _footerLabel = new Label { Text = $"buymeacoffee.com/{Username}", FontSize = 12, TextColor = Colors.Gray, VerticalOptions = LayoutOptions.Center, TextDecorations = TextDecorations.Underline, }; var footerStack = new HorizontalStackLayout { Spacing = 4, HorizontalOptions = LayoutOptions.Center, Children = { footerCup, _footerLabel } }; var footerTap = new TapGestureRecognizer(); footerTap.Tapped += OnFooterTapped; footerStack.GestureRecognizers.Add(footerTap); // Card layout var cardContent = new VerticalStackLayout { Spacing = 12, Padding = new Thickness(20), Children = { _headerLabel, amountBorder, nameBorder, messageBorder, _monthlyRow, _supportButton, redirectNote, footerStack, } }; var card = new Border { StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(BmcConstants.WidgetCornerRadius) }, Stroke = new SolidColorBrush(Color.FromArgb("#E0E0E0")), StrokeThickness = 1, BackgroundColor = Colors.White, Shadow = new Shadow { Brush = new SolidColorBrush(Colors.Black), Offset = new Point(0, 4), Radius = 12, Opacity = 0.10f, }, Content = cardContent, MaximumWidthRequest = 380, }; Content = card; } private Border CreateFieldBorder(View content) { return new Border { StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(10) }, Stroke = new SolidColorBrush(Color.FromArgb("#E0E0E0")), StrokeThickness = 1, Padding = new Thickness(12, 6), Content = content, }; } private void BuildChips() { _chipsRow.Children.Clear(); foreach (var amount in SuggestedAmounts) { var chipLabel = new Label { Text = $"+{amount}", FontSize = 13, FontAttributes = FontAttributes.Bold, TextColor = Color.FromArgb(BmcColors.BrandDark), VerticalOptions = LayoutOptions.Center, Padding = new Thickness(10, 4), }; var chip = new Border { StrokeShape = new RoundRectangle { CornerRadius = new Microsoft.Maui.CornerRadius(6) }, Stroke = new SolidColorBrush(Color.FromArgb("#E0E0E0")), StrokeThickness = 1, BackgroundColor = Color.FromArgb("#F5F5F5"), Content = chipLabel, VerticalOptions = LayoutOptions.Center, }; var chipTap = new TapGestureRecognizer(); var capturedAmount = amount; chipTap.Tapped += (_, _) => { _currentAmount = capturedAmount; _amountEntry.Text = _currentAmount.ToString(); }; chip.GestureRecognizers.Add(chipTap); _chipsRow.Children.Add(chip); } } private void OnAmountTextChanged(object? sender, TextChangedEventArgs e) { if (int.TryParse(e.NewTextValue, out var amount) && amount >= 0) _currentAmount = amount; } private async void OnSupportTapped(object? sender, TappedEventArgs e) { if (string.IsNullOrWhiteSpace(Username)) return; // Fire event with collected data SupportRequested?.Invoke(this, new SupportRequestedEventArgs { Username = Username, Amount = _currentAmount, Name = string.IsNullOrWhiteSpace(_nameEntry.Text) ? null : _nameEntry.Text, Message = string.IsNullOrWhiteSpace(_messageEditor.Text) ? null : _messageEditor.Text, IsMonthly = _monthlyCheckBox.IsChecked, }); // Animate await _supportButton.ScaleToAsync(0.95, 80, Easing.CubicIn); await _supportButton.ScaleToAsync(1.0, 80, Easing.CubicOut); // Open browser await Launcher.Default.OpenAsync(new Uri($"{BmcConstants.BaseUrl}{Username}")); } private async void OnFooterTapped(object? sender, TappedEventArgs e) { if (string.IsNullOrWhiteSpace(Username)) return; await Launcher.Default.OpenAsync(new Uri($"{BmcConstants.BaseUrl}{Username}")); } #region Property Changed Handlers private static void OnUsernameChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is BuyMeACoffeeWidget widget) widget._footerLabel.Text = $"buymeacoffee.com/{newValue}"; } private static void OnDisplayNameChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is BuyMeACoffeeWidget widget) { var name = (string)newValue; widget._headerLabel.Text = string.IsNullOrWhiteSpace(name) ? "Support" : $"Support {name}"; } } private static void OnChipsChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is BuyMeACoffeeWidget widget) widget.BuildChips(); } private static void OnDefaultAmountChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is BuyMeACoffeeWidget widget) { widget._currentAmount = (int)newValue; widget._amountEntry.Text = widget._currentAmount.ToString(); } } private static void OnAccentChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is BuyMeACoffeeWidget widget) widget._supportButton.BackgroundColor = (Color)newValue; } private static void OnShowMonthlyChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is BuyMeACoffeeWidget widget) widget._monthlyRow.IsVisible = (bool)newValue; } #endregion }