TASK 10: Polar billing + Velopack auto-updates (steps 1-7)
This commit is contained in:
@@ -1027,6 +1027,76 @@ public class LayoutStore : IDisposable
|
||||
return (string)cmd.ExecuteScalar()!;
|
||||
}
|
||||
|
||||
// ─── License (TASK 10 — Polar billing) ───
|
||||
|
||||
/// <summary>The saved license key, or null if never entered.</summary>
|
||||
public string? LoadLicenseKey()
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Value FROM Settings WHERE Key = 'LicenseKey';";
|
||||
var value = cmd.ExecuteScalar() as string;
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
public void SaveLicenseKey(string? key)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM Settings WHERE Key = 'LicenseKey';";
|
||||
cmd.ExecuteNonQuery();
|
||||
if (string.IsNullOrWhiteSpace(key)) return;
|
||||
|
||||
using var insert = _connection.CreateCommand();
|
||||
insert.CommandText = "INSERT INTO Settings (Key, Value) VALUES ('LicenseKey', $value);";
|
||||
insert.Parameters.AddWithValue("$value", key);
|
||||
insert.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
/// <summary>When the license was last validated online (ISO 8601), or null if never.</summary>
|
||||
public DateTime? LoadLicenseValidatedAt()
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Value FROM Settings WHERE Key = 'LicenseValidatedAt';";
|
||||
var value = cmd.ExecuteScalar() as string;
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
return DateTime.TryParse(value, out var dt) ? dt : null;
|
||||
}
|
||||
|
||||
public void SaveLicenseValidatedAt(DateTime? dt)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM Settings WHERE Key = 'LicenseValidatedAt';";
|
||||
cmd.ExecuteNonQuery();
|
||||
if (dt == null) return;
|
||||
|
||||
using var insert = _connection.CreateCommand();
|
||||
insert.CommandText = "INSERT INTO Settings (Key, Value) VALUES ('LicenseValidatedAt', $value);";
|
||||
insert.Parameters.AddWithValue("$value", dt.Value.ToString("o"));
|
||||
insert.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
/// <summary>Cached premium state (1 = premium, 0 = free). Used for offline
|
||||
/// grace period — the app checks online at startup but uses this cached
|
||||
/// value when offline.</summary>
|
||||
public bool LoadIsPremium()
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "SELECT Value FROM Settings WHERE Key = 'IsPremium';";
|
||||
var value = cmd.ExecuteScalar() as string;
|
||||
return value == "1";
|
||||
}
|
||||
|
||||
public void SaveIsPremium(bool isPremium)
|
||||
{
|
||||
using var cmd = _connection.CreateCommand();
|
||||
cmd.CommandText = "DELETE FROM Settings WHERE Key = 'IsPremium';";
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
using var insert = _connection.CreateCommand();
|
||||
insert.CommandText = "INSERT INTO Settings (Key, Value) VALUES ('IsPremium', $value);";
|
||||
insert.Parameters.AddWithValue("$value", isPremium ? "1" : "0");
|
||||
insert.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_connection.Dispose();
|
||||
|
||||
@@ -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