using System.Net.Http.Json; using System.Text.Json; using IronLicensing.Client.Exceptions; using IronLicensing.Client.Models; using Microsoft.Extensions.Options; namespace IronLicensing.Client; public class LicenseManager : ILicenseManager { private readonly HttpClient _httpClient; private readonly LicensingOptions _options; private readonly IMachineIdentifier _machineIdentifier; private readonly ILicenseCache _cache; private readonly ISignatureVerifier _signatureVerifier; private LicenseInfo? _currentLicense; private readonly SemaphoreSlim _syncLock = new(1, 1); public LicenseInfo? CurrentLicense => _currentLicense; public LicenseStatus Status => _currentLicense?.Status ?? LicenseStatus.Unknown; public bool IsLicensed => _currentLicense != null && (_currentLicense.Status == LicenseStatus.Active || _currentLicense.Status == LicenseStatus.Trial); public bool IsTrial => _currentLicense?.Status == LicenseStatus.Trial; public int TrialDaysRemaining { get { if (_currentLicense?.Status != LicenseStatus.Trial || !_currentLicense.ExpiresAt.HasValue) return 0; var remaining = (_currentLicense.ExpiresAt.Value - DateTime.UtcNow).Days; return Math.Max(0, remaining); } } public event EventHandler? LicenseChanged; public event EventHandler? ValidationCompleted; public LicenseManager( HttpClient httpClient, IOptions options, IMachineIdentifier machineIdentifier, ILicenseCache cache, ISignatureVerifier signatureVerifier) { _httpClient = httpClient; _options = options.Value; _machineIdentifier = machineIdentifier; _cache = cache; _signatureVerifier = signatureVerifier; ConfigureHttpClient(); } private void ConfigureHttpClient() { _httpClient.BaseAddress = new Uri(_options.ApiBaseUrl.TrimEnd('/') + "/"); _httpClient.DefaultRequestHeaders.Add("X-Public-Key", _options.PublicKey); _httpClient.DefaultRequestHeaders.Add("X-Product-Slug", _options.ProductSlug); _httpClient.Timeout = _options.HttpTimeout; } public async Task ValidateAsync(string? licenseKey = null) { await _syncLock.WaitAsync(); try { var key = licenseKey ?? _currentLicense?.LicenseKey; if (string.IsNullOrEmpty(key)) { // Try to load from cache var cached = await _cache.GetCachedLicenseAsync(); if (cached != null) { key = cached.License.LicenseKey; } } if (string.IsNullOrEmpty(key)) { return LicenseResult.Fail("NO_LICENSE", "No license key provided"); } try { var request = new { licenseKey = key, machineId = _machineIdentifier.GetMachineId(), machineName = _machineIdentifier.GetMachineName(), platform = _machineIdentifier.GetPlatform(), appVersion = GetAppVersion() }; var response = await _httpClient.PostAsJsonAsync("licenses/validate", request); var result = await response.Content.ReadFromJsonAsync(); if (result == null) { return await HandleOfflineValidation(key); } if (result.Valid && result.License != null) { var licenseInfo = MapToLicenseInfo(key, result.License); await UpdateLicense(licenseInfo); // Cache with signature for offline validation var rawJson = JsonSerializer.Serialize(result.License); await _cache.SaveLicenseAsync(licenseInfo, result.Signature, result.SigningPublicKey); // Save signing public key if provided if (!string.IsNullOrEmpty(result.SigningPublicKey)) { await _cache.SaveSigningPublicKeyAsync(result.SigningPublicKey); } RaiseValidationCompleted(true, licenseInfo, null, null); return LicenseResult.Ok(licenseInfo); } else { RaiseValidationCompleted(false, null, result.Error?.Code, result.Error?.Message); return LicenseResult.Fail(result.Error?.Code ?? "UNKNOWN", result.Error?.Message ?? "Validation failed"); } } catch (HttpRequestException) { return await HandleOfflineValidation(key); } catch (TaskCanceledException) { return await HandleOfflineValidation(key); } } finally { _syncLock.Release(); } } private async Task HandleOfflineValidation(string licenseKey) { var cached = await _cache.GetCachedLicenseAsync(); if (cached == null || cached.License.LicenseKey != licenseKey) { RaiseValidationCompleted(false, null, "OFFLINE", "Cannot validate license while offline"); return LicenseResult.Fail("OFFLINE", "Cannot validate license while offline"); } // Check offline grace period var daysSinceValidation = (DateTime.UtcNow - cached.License.LastValidated).TotalDays; if (daysSinceValidation > _options.OfflineGraceDays) { RaiseValidationCompleted(false, null, "OFFLINE_EXPIRED", "Offline grace period expired. Please connect to the internet to validate your license."); return LicenseResult.Fail("OFFLINE_EXPIRED", "Offline grace period expired. Please connect to the internet to validate your license."); } // Verify signature if required if (_options.RequireSignatureForOffline) { var publicKey = _options.SigningPublicKey ?? cached.SigningPublicKey ?? await _cache.GetSigningPublicKeyAsync(); if (string.IsNullOrEmpty(publicKey) || string.IsNullOrEmpty(cached.Signature) || string.IsNullOrEmpty(cached.RawLicenseJson)) { // No signature available - check if we allow this if (_options.RequireSignatureForOffline) { RaiseValidationCompleted(false, null, "NO_SIGNATURE", "Offline validation requires a signed license"); return LicenseResult.Fail("NO_SIGNATURE", "Offline validation requires a signed license"); } } else { // Verify signature var isValid = _signatureVerifier.Verify(publicKey, cached.RawLicenseJson, cached.Signature); if (!isValid) { RaiseValidationCompleted(false, null, "INVALID_SIGNATURE", "License signature verification failed"); return LicenseResult.Fail("INVALID_SIGNATURE", "License signature verification failed"); } } } // Valid offline license await UpdateLicense(cached.License); RaiseValidationCompleted(true, cached.License, null, null); return LicenseResult.Ok(cached.License); } public async Task ActivateAsync(string licenseKey) { await _syncLock.WaitAsync(); try { var request = new { licenseKey, machineId = _machineIdentifier.GetMachineId(), machineName = _machineIdentifier.GetMachineName(), platform = _machineIdentifier.GetPlatform(), appVersion = GetAppVersion() }; var response = await _httpClient.PostAsJsonAsync("licenses/activate", request); var result = await response.Content.ReadFromJsonAsync(); if (result?.Success == true) { // Release lock before calling ValidateAsync (it acquires its own lock) _syncLock.Release(); try { return await ValidateAsync(licenseKey); } finally { await _syncLock.WaitAsync(); } } return LicenseResult.Fail(result?.Error?.Code ?? "UNKNOWN", result?.Error?.Message ?? "Activation failed"); } catch (Exception ex) { return LicenseResult.Fail("ERROR", ex.Message); } finally { _syncLock.Release(); } } public async Task DeactivateAsync() { if (_currentLicense == null) return false; await _syncLock.WaitAsync(); try { var request = new { licenseKey = _currentLicense.LicenseKey, machineId = _machineIdentifier.GetMachineId() }; var response = await _httpClient.PostAsJsonAsync("licenses/deactivate", request); var result = await response.Content.ReadFromJsonAsync(); if (result?.Success == true) { await ClearCacheAsync(); await UpdateLicense(null); return true; } return false; } catch { return false; } finally { _syncLock.Release(); } } public async Task StartPurchaseAsync(string tierId, string customerEmail) { try { var request = new { tierId, customerEmail, successUrl = _options.SuccessUrl, cancelUrl = _options.CancelUrl, metadata = new Dictionary { ["machineId"] = _machineIdentifier.GetMachineId() } }; var response = await _httpClient.PostAsJsonAsync("checkout/create", request); var result = await response.Content.ReadFromJsonAsync(); if (result != null) { return new CheckoutResult { Success = true, SessionId = result.SessionId, CheckoutUrl = result.CheckoutUrl, ExpiresAt = result.ExpiresAt }; } return new CheckoutResult { Success = false, ErrorMessage = "Failed to create checkout session" }; } catch (Exception ex) { return new CheckoutResult { Success = false, ErrorMessage = ex.Message }; } } public bool HasFeature(string featureKey) { if (_currentLicense == null) return false; return _currentLicense.Features.Contains(featureKey) || _currentLicense.Features.Contains("*"); } public async Task HasFeatureAsync(string featureKey) { if (_currentLicense == null) return false; // Check local first if (HasFeature(featureKey)) return true; // Check with server try { var response = await _httpClient.GetAsync($"licenses/{_currentLicense.LicenseKey}/features/{featureKey}"); if (response.IsSuccessStatusCode) { var result = await response.Content.ReadFromJsonAsync(); return result.TryGetProperty("enabled", out var enabled) && enabled.GetBoolean(); } } catch { // Fall back to local check } return false; } public void RequireFeature(string featureKey) { if (!HasFeature(featureKey)) { throw new LicenseRequiredException(featureKey); } } public async Task StartTrialAsync(string email) { try { var request = new { email, machineId = _machineIdentifier.GetMachineId(), machineName = _machineIdentifier.GetMachineName(), platform = _machineIdentifier.GetPlatform() }; var response = await _httpClient.PostAsJsonAsync("trial/start", request); var result = await response.Content.ReadFromJsonAsync(); if (result?.Success == true && !string.IsNullOrEmpty(result.LicenseKey)) { // Activate the trial license var activationResult = await ActivateAsync(result.LicenseKey); return activationResult.Success; } return false; } catch { return false; } } public async Task ClearCacheAsync() { await _cache.ClearAsync(); } private async Task UpdateLicense(LicenseInfo? license) { var oldLicense = _currentLicense; _currentLicense = license; LicenseChanged?.Invoke(this, new LicenseChangedEventArgs { OldLicense = oldLicense, NewLicense = license }); await Task.CompletedTask; } private void RaiseValidationCompleted(bool success, LicenseInfo? license, string? errorCode, string? errorMessage) { ValidationCompleted?.Invoke(this, new LicenseValidationEventArgs { Success = success, License = license, ErrorCode = errorCode, ErrorMessage = errorMessage }); } private static LicenseInfo MapToLicenseInfo(string licenseKey, ValidationLicenseInfo validation) { return new LicenseInfo { LicenseKey = licenseKey, Tier = validation.Tier, Status = ParseStatus(validation.Status), ExpiresAt = validation.ExpiresAt, Features = validation.Features, ActivationsUsed = validation.ActivationsUsed, ActivationsMax = validation.ActivationsMax, LastValidated = DateTime.UtcNow }; } private static LicenseStatus ParseStatus(string status) { return status.ToLowerInvariant() switch { "trial" => LicenseStatus.Trial, "active" => LicenseStatus.Active, "expired" => LicenseStatus.Expired, "suspended" => LicenseStatus.Suspended, "revoked" => LicenseStatus.Revoked, _ => LicenseStatus.Unknown }; } private static string GetAppVersion() { var assembly = System.Reflection.Assembly.GetEntryAssembly(); return assembly?.GetName().Version?.ToString() ?? "1.0.0"; } }