diff --git a/ViewModels/MainViewModel.License.cs b/ViewModels/MainViewModel.License.cs new file mode 100644 index 0000000..fcf764a --- /dev/null +++ b/ViewModels/MainViewModel.License.cs @@ -0,0 +1,162 @@ +using ytLive.Helpers; +using ytLive.Services; + +namespace ytLive.ViewModels; + +public partial class MainViewModel +{ + // TASK 10 — Polar license validation + private string _licenseKey = string.Empty; + private string _licenseMessage = string.Empty; + private bool _isLicenseValidating; + private bool _isLicenseEntryOpen; + private static readonly TimeSpan OfflineGracePeriod = TimeSpan.FromDays(14); + + /// True when the user holds a valid LlamaCasty license (Polar). When + /// false, the branding flash runs on every live stream. + private bool _isPremium; + public bool IsPremium + { + get => _isPremium; + private set + { + if (!SetProperty(ref _isPremium, value)) return; + // Flip the branding flash when premium state changes. + BrandFlashEnabled = !value; + } + } + + // TASK 10 — license key entry + public string LicenseKey + { + get => _licenseKey; + set => SetProperty(ref _licenseKey, value); + } + + public string LicenseMessage + { + get => _licenseMessage; + private set => SetProperty(ref _licenseMessage, value); + } + + public bool IsLicenseValidating + { + get => _isLicenseValidating; + private set => SetProperty(ref _isLicenseValidating, value); + } + + public bool IsLicenseEntryOpen + { + get => _isLicenseEntryOpen; + set + { + if (SetProperty(ref _isLicenseEntryOpen, value)) + OnPropertyChanged(nameof(IsHubVisible)); + } + } + + /// The About hub is visible only when neither the licenses panel + /// nor the license entry panel is open. + public bool IsHubVisible => !IsLicensesOpen && !IsLicenseEntryOpen; + + // ─── TASK 10 — Polar license validation ─── + + /// At startup, load the cached license state and re-validate online + /// if a key exists. If offline and within the 14-day grace period, the cached + /// premium state is trusted. If the grace period is exceeded, premium lapses. + private async Task ValidateLicenseAtStartupAsync() + { + var savedKey = _layoutStore.LoadLicenseKey(); + if (string.IsNullOrWhiteSpace(savedKey)) + { + IsPremium = false; + return; + } + + LicenseKey = savedKey; + var validatedAt = _layoutStore.LoadLicenseValidatedAt(); + var cachedPremium = _layoutStore.LoadIsPremium(); + + // If the last validation was within the grace period, trust the cache + // while we attempt an online re-validation in the background. + if (validatedAt.HasValue && DateTime.UtcNow - validatedAt.Value < OfflineGracePeriod) + { + IsPremium = cachedPremium; + } + + // Try an online check — if it succeeds, it overrides the cache. + try + { + var service = new PolarLicenseService(); + var result = await service.ValidateAsync(savedKey); + if (result.IsValid) + { + IsPremium = true; + _layoutStore.SaveIsPremium(true); + _layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow); + } + else + { + // Key revoked or expired — clear premium. + IsPremium = false; + _layoutStore.SaveIsPremium(false); + _layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow); + AppLog.Write($"License validation failed: {result.Error}"); + _notifications.Warning("Premium is no longer active", + $"{result.Error} — watermark branding returns until the license is renewed."); + } + } + catch (Exception ex) + { + // Offline or network error — keep the cached state, log it. + AppLog.Write($"License re-validation skipped (offline?): {ex.Message}"); + if (!validatedAt.HasValue || DateTime.UtcNow - validatedAt.Value >= OfflineGracePeriod) + { + IsPremium = false; + _layoutStore.SaveIsPremium(false); + } + } + } + + /// Called from the Activate button in the About overlay. Validates + /// the entered key, persists it, and flips premium state. + private async Task ActivateLicenseAsync() + { + if (string.IsNullOrWhiteSpace(LicenseKey)) return; + + IsLicenseValidating = true; + LicenseMessage = "Validating\u2026"; + + try + { + var service = new PolarLicenseService(); + var result = await service.ValidateAsync(LicenseKey); + if (result.IsValid) + { + IsPremium = true; + _layoutStore.SaveLicenseKey(LicenseKey.Trim()); + _layoutStore.SaveIsPremium(true); + _layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow); + LicenseMessage = "Premium activated! The watermark has been removed."; + AppLog.Write("License activated successfully"); + } + else + { + IsPremium = false; + _layoutStore.SaveLicenseKey(null); + _layoutStore.SaveIsPremium(false); + LicenseMessage = result.Error ?? "Invalid license key."; + AppLog.Write($"License activation failed: {result.Error}"); + } + } + catch (Exception ex) + { + LicenseMessage = $"Activation error: {ex.Message}"; + AppLog.Write($"License activation error: {ex}"); + } + finally + { + IsLicenseValidating = false; + } + } +} \ No newline at end of file diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index cd0ba3c..fdac296 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -173,13 +173,6 @@ public partial class MainViewModel : ViewModelBase // TASK 20 step 2 — hotkey bindings (custom chord per action) private Dictionary _hotkeyBindings = new(); - // TASK 10 — Polar license validation - private string _licenseKey = string.Empty; - private string _licenseMessage = string.Empty; - private bool _isLicenseValidating; - private bool _isLicenseEntryOpen; - private static readonly TimeSpan OfflineGracePeriod = TimeSpan.FromDays(14); - private const string SupportEmail = "gramps@llamachile.shop"; // The creator-hub link targets (About overlay) — public so the hub's // integration test can assert they point at real destinations. @@ -467,20 +460,6 @@ public partial class MainViewModel : ViewModelBase /// The global socials config (entries + bar settings), or null. public SocialsConfig? Socials => _socials; - /// True when the user holds a valid LlamaCasty license (Polar). When - /// false, the branding flash runs on every live stream. - private bool _isPremium; - public bool IsPremium - { - get => _isPremium; - private set - { - if (!SetProperty(ref _isPremium, value)) return; - // Flip the branding flash when premium state changes. - BrandFlashEnabled = !value; - } - } - /// Canvas.Top for the social bar: 0 = top, 1040 = bottom (40px from the 1080 edge). public double SocialBarTop => _socials?.BarPosition == SocialBarPosition.Top ? 0 : 1040; @@ -1511,39 +1490,6 @@ public partial class MainViewModel : ViewModelBase set => SetProperty(ref _featureRequestEmail, value); } - // TASK 10 — license key entry - public string LicenseKey - { - get => _licenseKey; - set => SetProperty(ref _licenseKey, value); - } - - public string LicenseMessage - { - get => _licenseMessage; - private set => SetProperty(ref _licenseMessage, value); - } - - public bool IsLicenseValidating - { - get => _isLicenseValidating; - private set => SetProperty(ref _isLicenseValidating, value); - } - - public bool IsLicenseEntryOpen - { - get => _isLicenseEntryOpen; - set - { - if (SetProperty(ref _isLicenseEntryOpen, value)) - OnPropertyChanged(nameof(IsHubVisible)); - } - } - - /// The About hub is visible only when neither the licenses panel - /// nor the license entry panel is open. - public bool IsHubVisible => !IsLicensesOpen && !IsLicenseEntryOpen; - // Commands public ICommand AddSceneCommand { get; } public ICommand AddSourceCommand { get; } @@ -1750,107 +1696,6 @@ public partial class MainViewModel : ViewModelBase AppLog.Write("MainViewModel ctor end"); } - // ─── TASK 10 — Polar license validation ─── - - /// At startup, load the cached license state and re-validate online - /// if a key exists. If offline and within the 14-day grace period, the cached - /// premium state is trusted. If the grace period is exceeded, premium lapses. - private async Task ValidateLicenseAtStartupAsync() - { - var savedKey = _layoutStore.LoadLicenseKey(); - if (string.IsNullOrWhiteSpace(savedKey)) - { - IsPremium = false; - return; - } - - LicenseKey = savedKey; - var validatedAt = _layoutStore.LoadLicenseValidatedAt(); - var cachedPremium = _layoutStore.LoadIsPremium(); - - // If the last validation was within the grace period, trust the cache - // while we attempt an online re-validation in the background. - if (validatedAt.HasValue && DateTime.UtcNow - validatedAt.Value < OfflineGracePeriod) - { - IsPremium = cachedPremium; - } - - // Try an online check — if it succeeds, it overrides the cache. - try - { - var service = new PolarLicenseService(); - var result = await service.ValidateAsync(savedKey); - if (result.IsValid) - { - IsPremium = true; - _layoutStore.SaveIsPremium(true); - _layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow); - } - else - { - // Key revoked or expired — clear premium. - IsPremium = false; - _layoutStore.SaveIsPremium(false); - _layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow); - AppLog.Write($"License validation failed: {result.Error}"); - _notifications.Warning("Premium is no longer active", - $"{result.Error} — watermark branding returns until the license is renewed."); - } - } - catch (Exception ex) - { - // Offline or network error — keep the cached state, log it. - AppLog.Write($"License re-validation skipped (offline?): {ex.Message}"); - if (!validatedAt.HasValue || DateTime.UtcNow - validatedAt.Value >= OfflineGracePeriod) - { - IsPremium = false; - _layoutStore.SaveIsPremium(false); - } - } - } - - /// Called from the Activate button in the About overlay. Validates - /// the entered key, persists it, and flips premium state. - private async Task ActivateLicenseAsync() - { - if (string.IsNullOrWhiteSpace(LicenseKey)) return; - - IsLicenseValidating = true; - LicenseMessage = "Validating\u2026"; - - try - { - var service = new PolarLicenseService(); - var result = await service.ValidateAsync(LicenseKey); - if (result.IsValid) - { - IsPremium = true; - _layoutStore.SaveLicenseKey(LicenseKey.Trim()); - _layoutStore.SaveIsPremium(true); - _layoutStore.SaveLicenseValidatedAt(DateTime.UtcNow); - LicenseMessage = "Premium activated! The watermark has been removed."; - AppLog.Write("License activated successfully"); - } - else - { - IsPremium = false; - _layoutStore.SaveLicenseKey(null); - _layoutStore.SaveIsPremium(false); - LicenseMessage = result.Error ?? "Invalid license key."; - AppLog.Write($"License activation failed: {result.Error}"); - } - } - catch (Exception ex) - { - LicenseMessage = $"Activation error: {ex.Message}"; - AppLog.Write($"License activation error: {ex}"); - } - finally - { - IsLicenseValidating = false; - } - } - private static string DefaultLayoutPath => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ytLlive", diff --git a/ViewModels/index.md b/ViewModels/index.md index 5f9408e..08a6ebb 100644 --- a/ViewModels/index.md +++ b/ViewModels/index.md @@ -27,12 +27,15 @@ Progress (Commit N/12 · tag): - **Commit 1/12 · `refactor-commit-1`: Account landed** → `MainViewModel.Account.cs` (session restore `LoadSavedSessionAsync`, sign-in/out/change account, `IsConnected`/`AccountAvatarUrl`/`AccountDisplayName`/`AccountStatusLightToolTip`). +- **Commit 2/12 · `refactor-commit-2`: License landed** → `MainViewModel.License.cs` + (Polar license state: `LicenseKey`/`LicenseMessage`/`IsLicenseValidating`/ + `IsLicenseEntryOpen`/`IsHubVisible`/`IsPremium`, `OfflineGracePeriod`, + `ValidateLicenseAtStartupAsync`, `ActivateLicenseAsync`). - Core file keeps: fields, ctor, `Shutdown`/`SaveLayoutNow`/`ScheduleSave`/`LoadLayout`, `SetPanel`/`ShowOverlay`/`ShowLicenses`, `CreateBrush`, `OpenUrl`/`ComposeEmail`, `SubmitBug`/`SubmitFeature`, window-geometry pass-through. -- Partials: `Account` ✔ (Commit 1), `License` (Polar key validation + `IsPremium`), - `Scenes` (scene catalog + hotkeys + transitions), `Background` (background model + screen +- Partials: `Account` ✔ (Commit 1), `License` ✔ (Commit 2), `Scenes` (scene catalog + hotkeys + transitions), `Background` (background model + screen capture + display picker), `Webcam` (webcam identity + per-scene configs + captures), `Audio` (mic + game bars + TRAX), `Trax` (music player), `Socials` (social bar + dialog), `Streaming` (go-live/record + health + quality tiers + media source pipeline),