Limited
2
0
Files
bmc.maui/BuyMeCofee.Maui/Controls/BuyMeACoffeeWidget.cs

494 lines
17 KiB
C#
Raw Normal View History

using BuyMeCofee.Maui.Constants;
using BuyMeCofee.Maui.Helpers;
using BuyMeCofee.Maui.Models;
using Microsoft.Maui.Controls.Shapes;
namespace BuyMeCofee.Maui.Controls;
/// <summary>
/// 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.
/// </summary>
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);
public static readonly BindableProperty CustomLogoSourceProperty =
BindableProperty.Create(nameof(CustomLogoSource), typeof(ImageSource), typeof(BuyMeACoffeeWidget),
null, propertyChanged: OnLogoChanged);
public static readonly BindableProperty SupporterEmailProperty =
BindableProperty.Create(nameof(SupporterEmail), typeof(string), typeof(BuyMeACoffeeWidget),
null, propertyChanged: OnSupporterPropertyChanged);
public static readonly BindableProperty HideIfSupporterProperty =
BindableProperty.Create(nameof(HideIfSupporter), typeof(bool), typeof(BuyMeACoffeeWidget),
false, propertyChanged: OnSupporterPropertyChanged);
#endregion
#region Properties
/// <summary>Your Buy Me a Coffee username/slug.</summary>
public string Username
{
get => (string)GetValue(UsernameProperty);
set => SetValue(UsernameProperty, value);
}
/// <summary>Name displayed in "Support {DisplayName}" header.</summary>
public string DisplayName
{
get => (string)GetValue(DisplayNameProperty);
set => SetValue(DisplayNameProperty, value);
}
/// <summary>Preset amount chips. Default: [25, 50, 100]</summary>
public int[] SuggestedAmounts
{
get => (int[])GetValue(SuggestedAmountsProperty);
set => SetValue(SuggestedAmountsProperty, value);
}
/// <summary>Initial amount in the entry field. Default: 5</summary>
public int DefaultAmount
{
get => (int)GetValue(DefaultAmountProperty);
set => SetValue(DefaultAmountProperty, value);
}
/// <summary>Accent color for the Support button. Default: BMC purple (#6C5CE7)</summary>
public Color AccentColor
{
get => (Color)GetValue(AccentColorProperty);
set => SetValue(AccentColorProperty, value);
}
/// <summary>Whether to show the "Make this monthly" checkbox. Default: true</summary>
public bool ShowMonthlyOption
{
get => (bool)GetValue(ShowMonthlyOptionProperty);
set => SetValue(ShowMonthlyOptionProperty, value);
}
/// <summary>Text on the support button. Default: "Support"</summary>
public string SupportButtonText
{
get => (string)GetValue(SupportButtonTextProperty);
set => SetValue(SupportButtonTextProperty, value);
}
/// <summary>Optional custom logo ImageSource. When set, replaces the default BMC cup logo in the footer.</summary>
public ImageSource? CustomLogoSource
{
get => (ImageSource?)GetValue(CustomLogoSourceProperty);
set => SetValue(CustomLogoSourceProperty, value);
}
/// <summary>
/// Email address to check against your BMC supporters list.
/// Requires AccessToken configured via UseBuyMeACoffee(options => ...).
/// </summary>
public string? SupporterEmail
{
get => (string?)GetValue(SupporterEmailProperty);
set => SetValue(SupporterEmailProperty, value);
}
/// <summary>
/// When true, the control auto-hides if SupporterEmail is a verified supporter. Default: false.
/// </summary>
public bool HideIfSupporter
{
get => (bool)GetValue(HideIfSupporterProperty);
set => SetValue(HideIfSupporterProperty, value);
}
#endregion
/// <summary>Raised when the user taps the Support button, before opening the browser.</summary>
public event EventHandler<SupportRequestedEventArgs>? 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 Image _footerCup = 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
_footerCup = new Image
{
Source = CustomLogoSource ?? 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;
}
private static void OnLogoChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is BuyMeACoffeeWidget widget)
widget._footerCup.Source = (ImageSource?)newValue ?? BmcBrandAssets.GetCupLogo();
}
private static void OnSupporterPropertyChanged(BindableObject bindable, object oldValue, object newValue)
{
if (bindable is BuyMeACoffeeWidget widget)
BmcSupporterCheck.CheckAndHide(widget, widget.SupporterEmail, widget.HideIfSupporter);
}
#endregion
}