Limited
2
0

feat(ci): add supporter verification and auto-hide functionality
Some checks failed
Release / Verify Apple Build (push) Successful in 14s
Release / Build & Pack (push) Successful in 8h59m34s
Release / Publish (push) Failing after 16s

Implement BmcSupporterService to verify supporters via BMC API with configurable caching. Add SupporterEmail and HideIfSupporter properties to all controls to automatically hide donation prompts for existing supporters. Replace PNG logo with SVG for crisp rendering at all scales. Add BmcOptions and BmcConfiguration for library-wide settings. Bump version to 1.1.0.
This commit is contained in:
2026-03-04 00:35:05 -05:00
parent 7ec645b9a6
commit 66c8ec8ecf
12 changed files with 436 additions and 329 deletions

View File

@@ -0,0 +1,93 @@
using System.Net.Http.Headers;
using System.Text.Json;
namespace BuyMeCofee.Maui.Services;
/// <summary>
/// Checks the Buy Me a Coffee API to determine whether a given email
/// belongs to a supporter (one-time or member). Results are cached.
/// </summary>
public class BmcSupporterService
{
private const string ApiBase = "https://developers.buymeacoffee.com/api/v1/";
private readonly HttpClient _http;
private readonly TimeSpan _cacheDuration;
private HashSet<string>? _cachedEmails;
private DateTime _cacheExpiry;
public BmcSupporterService(string accessToken, TimeSpan cacheDuration)
{
_cacheDuration = cacheDuration;
_http = new HttpClient { BaseAddress = new Uri(ApiBase) };
_http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
}
/// <summary>
/// Returns true if the given email matches any one-time supporter
/// or active/inactive member on the creator's BMC account.
/// </summary>
public async Task<bool> IsSupporterAsync(string email)
{
if (string.IsNullOrWhiteSpace(email)) return false;
var emails = await GetAllSupporterEmailsAsync();
return emails.Contains(email.Trim());
}
private async Task<HashSet<string>> GetAllSupporterEmailsAsync()
{
if (_cachedEmails != null && DateTime.UtcNow < _cacheExpiry)
return _cachedEmails;
var emails = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
await FetchEmailsFromEndpoint("supporters", emails);
await FetchEmailsFromEndpoint("subscriptions", emails);
_cachedEmails = emails;
_cacheExpiry = DateTime.UtcNow + _cacheDuration;
return emails;
}
private async Task FetchEmailsFromEndpoint(string endpoint, HashSet<string> emails)
{
try
{
var page = 1;
while (true)
{
var response = await _http.GetAsync($"{endpoint}?page={page}");
if (!response.IsSuccessStatusCode) break;
var json = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (root.TryGetProperty("data", out var data))
{
foreach (var item in data.EnumerateArray())
{
if (item.TryGetProperty("payer_email", out var emailProp))
{
var email = emailProp.GetString();
if (!string.IsNullOrWhiteSpace(email))
emails.Add(email);
}
}
}
if (!root.TryGetProperty("next_page_url", out var nextPage) ||
nextPage.ValueKind == JsonValueKind.Null)
break;
page++;
}
}
catch
{
// Silently fail — don't block the app if BMC API is unreachable
}
}
}