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),