TASK 10: Polar billing + Velopack auto-updates (steps 1-7)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ytLive.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Validates LlamaCasty license keys against the Polar.sh customer-portal API.
|
||||
/// No auth required for client-side validation — the endpoint is public by design.
|
||||
/// </summary>
|
||||
public class PolarLicenseService
|
||||
{
|
||||
private const string ValidateUrl = "https://api.polar.sh/v1/customer-portal/license-keys/validate";
|
||||
public const string OrganizationId = "c05fb364-b967-4f6c-adf2-8a144e46d085";
|
||||
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public PolarLicenseService(HttpClient? http = null)
|
||||
{
|
||||
_http = http ?? new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
|
||||
}
|
||||
|
||||
public async Task<LicenseValidationResult> ValidateAsync(string key, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return new LicenseValidationResult { IsValid = false, Error = "Key is empty" };
|
||||
|
||||
try
|
||||
{
|
||||
var request = new { key = key.Trim(), organization_id = OrganizationId };
|
||||
var response = await _http.PostAsJsonAsync(ValidateUrl, request, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = false,
|
||||
Error = $"Validation failed ({(int)response.StatusCode}): {body}"
|
||||
};
|
||||
}
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<PolarValidateResponse>(ct);
|
||||
if (result == null)
|
||||
return new LicenseValidationResult { IsValid = false, Error = "Empty response from Polar" };
|
||||
|
||||
var status = result.Status?.ToLowerInvariant();
|
||||
return new LicenseValidationResult
|
||||
{
|
||||
IsValid = status == "granted",
|
||||
Status = status ?? "unknown",
|
||||
ExpiresAt = result.ExpiresAt,
|
||||
Error = status == "granted" ? null : $"License status: {status}"
|
||||
};
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return new LicenseValidationResult { IsValid = false, Error = "Validation timed out" };
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new LicenseValidationResult { IsValid = false, Error = $"Network error: {ex.Message}" };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new LicenseValidationResult { IsValid = false, Error = $"Unexpected error: {ex.Message}" };
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PolarValidateResponse
|
||||
{
|
||||
[JsonPropertyName("status")]
|
||||
public string? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("expires_at")]
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LicenseValidationResult
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public string? Status { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user