From 8303b1cf7fd41a5b6ab15e7f39a279c8a7529dc1 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 9 Oct 2026 13:22:52 -0700 Subject: [PATCH] Brand flash OFF by default: MainViewModel.ShowBrandFlash master switch (default false) gates BrandFlashEnabled; feature kept, tests opt in; new test guards the default (Unit: brand flash off) --- ViewModels/MainViewModel.License.cs | 2 +- ViewModels/MainViewModel.cs | 26 +++++++++++--------- ai.md | 9 +++++++ ytLive.Tests/BrandFlashOutputTests.cs | 34 +++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/ViewModels/MainViewModel.License.cs b/ViewModels/MainViewModel.License.cs index 522246d..9e83a5a 100644 --- a/ViewModels/MainViewModel.License.cs +++ b/ViewModels/MainViewModel.License.cs @@ -23,7 +23,7 @@ public partial class MainViewModel if (!SetProperty(ref _isPremium, value)) return; // The flash is composited into the broadcast, so the gate has to be pushed // all the way into the renderer — not just the preview. - _brandFlash.Enabled = !value; + _brandFlash.Enabled = ShowBrandFlash && !value; } } diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index d725538..1738803 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -51,11 +51,9 @@ public partial class MainViewModel : ViewModelBase private WebView2Manager? _webView2Manager; - // Branding flash (monetization): a full-frame "made with LlamaCasty!" shown - // for ~1s every 300s on the live output, ~25% opacity. Paid unlock sets - // BrandFlashEnabled = false. See ai.md "Monetization". - // The free-tier branding credit. Composed into the OUTPUT by FramePump, not just - // previewed — see BrandFlashPresenter. Gated on IsPremium (Polar), never a toggle. + // Branding credit: "made with LlamaCasty!" composed into the OUTPUT by FramePump, not + // just previewed — see BrandFlashPresenter. OFF by default since 2026-10-09 + // (ShowBrandFlash = false, free-forever: no branding on the stream); the code stays. private readonly BrandFlashPresenter _brandFlash; public SceneElement? SelectedElement @@ -122,11 +120,17 @@ public partial class MainViewModel : ViewModelBase public bool ShowPreviewPlaceholder => !ShowEmptySceneHint && ActiveBackgroundImage == null && BackgroundImage == null; - // Paid unlock flips this off (see ai.md "Monetization"). When disabled the - // presenter stops emitting, so any active credit dies on the next tick — and a - // credit already in flight is cut, not run out. Derived, never assignable: the - // whole point is that nobody can turn the advertising on a paying stream. - public bool BrandFlashEnabled => !IsPremium; + /// Master switch for the "made with LlamaCasty!" credit (creator ruling + /// 2026-10-09: turn it OFF, but keep the feature). false = the credit never + /// shows — not in the preview, the stream, or recordings. Read when the view model is + /// constructed (and on every licence change), so set it before a MainWindow is + /// created; the tests that exercise the credit opt in that way. + internal static bool ShowBrandFlash { get; set; } = false; + + // When disabled the presenter stops emitting, so any active credit dies on the next + // tick — and a credit already in flight is cut, not run out. Derived, never + // assignable from the UI: the master switch AND the licence must both allow it. + public bool BrandFlashEnabled => ShowBrandFlash && !IsPremium; // Window geometry pass-through (code-behind owns the WPF specifics). public (double Left, double Top, double Width, double Height, string State)? RestoreWindowState() @@ -184,7 +188,7 @@ public partial class MainViewModel : ViewModelBase // — go-live and local recording are the same FramePump, so one start covers // output, recording and the preview pane alike. Enabled is seeded from the // current licence state; ValidateLicenseAtStartupAsync re-pushes it later. - _brandFlash.Enabled = !IsPremium; + _brandFlash.Enabled = BrandFlashEnabled; _brandFlash.Start(); _healthPollTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(30) }; diff --git a/ai.md b/ai.md index ebcbc46..f3133ed 100644 --- a/ai.md +++ b/ai.md @@ -1767,6 +1767,15 @@ Apply this to every UI decision: ## Monetization (design decision — the branding flash is the sword) +> **⛔ Superseded 2026-10-08/09 — free forever, no branding on the stream.** The credit is now +> **OFF by default**: `MainViewModel.ShowBrandFlash` (internal static, default `false`) is the +> master switch, and `BrandFlashEnabled => ShowBrandFlash && !IsPremium` feeds the presenter at +> construction and on every licence change. The code (`BrandFlashPresenter`, the FramePump +> `brandFlash:` seam, the preview layer) is kept, not deleted — creator ruling: "don't delete it, +> just make it conditionally displayed". `BrandFlashOutputTests` opt in by setting the switch +> before constructing a `MainWindow`; `Credit_IsOffByDefault_EvenOnTheFreeTier` guards the default. +> Everything below is the historical paid-tier design. + Full pricing and the Year 1 revenue forecast in `MONETIZATION.md` (gitignored). ⚠️ **No Polar product, discount code, or license key exists any more** — billing is Microsoft Store IAP. Brand palette and assets in `MARCOM.md` (gitignored). This section covers the in-app model. diff --git a/ytLive.Tests/BrandFlashOutputTests.cs b/ytLive.Tests/BrandFlashOutputTests.cs index 69faa6b..7ad5722 100644 --- a/ytLive.Tests/BrandFlashOutputTests.cs +++ b/ytLive.Tests/BrandFlashOutputTests.cs @@ -164,6 +164,7 @@ public sealed class BrandFlashOutputTests { var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; + MainViewModel.ShowBrandFlash = true; // the credit is off by default (2026-10-09) var window = new MainWindow(); var vm = (MainViewModel)window.DataContext; try @@ -202,6 +203,7 @@ public sealed class BrandFlashOutputTests { window.Close(); MainViewModel.LayoutPathOverride = null; + MainViewModel.ShowBrandFlash = false; SqliteConnection.ClearAllPools(); try { File.Delete(tempDb); } catch { /* best-effort cleanup */ } } @@ -381,6 +383,36 @@ public sealed class BrandFlashOutputTests Assert.True(prop.CanRead, "the preview needs to read the gate state"); } + /// Creator ruling 2026-10-09: the credit is OFF by default — a freshly + /// constructed app with no licence still emits nothing to the preview or the output. + [Fact] + public void Credit_IsOffByDefault_EvenOnTheFreeTier() + { + _app.Run(() => + { + Assert.False(MainViewModel.ShowBrandFlash); + var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db"); + MainViewModel.LayoutPathOverride = tempDb; + var window = new MainWindow(); + var vm = (MainViewModel)window.DataContext; + try + { + Assert.False(vm.IsPremium); + Assert.False(vm.BrandFlashEnabled); + vm.AdvanceBrandFlash(120); // past several cadence windows + Assert.Null(vm.BrandFlashFrame()); + Assert.False(vm.BrandFlashVisible); + } + finally + { + window.Close(); + MainViewModel.LayoutPathOverride = null; + SqliteConnection.ClearAllPools(); + try { File.Delete(tempDb); } catch { /* best-effort cleanup */ } + } + }); + } + // ---------- the credit is NOT a go-live-only behaviour ---------- /// Reported 2026-09-26: "the made with llamacasty flash should appear in @@ -400,6 +432,7 @@ public sealed class BrandFlashOutputTests { var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-test-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; + MainViewModel.ShowBrandFlash = true; // the credit is off by default (2026-10-09) var window = new MainWindow(); var vm = (MainViewModel)window.DataContext; try @@ -441,6 +474,7 @@ public sealed class BrandFlashOutputTests { window.Close(); MainViewModel.LayoutPathOverride = null; + MainViewModel.ShowBrandFlash = false; SqliteConnection.ClearAllPools(); try { File.Delete(tempDb); } catch { /* best-effort cleanup */ } }