Replace MAUI asset pipeline logo loading with embedded resource stream to fix logo not appearing in consuming NuGet projects. Add CustomLogoSource property to Button and Widget controls for custom branding. Update logo to higher resolution PNG. Bump version to 1.1.2.
494 lines
17 KiB
C#
494 lines
17 KiB
C#
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
|
|
}
|