feat(about): rebuild the About box as a conversion surface
Creator: the About box was "shitty" — an unreadable logo, a version number nobody asked for, copy that insulted the creator, and buttons that looked like text. Rebuilt around one job: convert. - **820×860 modal** (was 650×700) with a scrollable hub, and the logo at **520 px wide** instead of 125 — the asset is 2816×1536, so it was being drawn as a thumbnail, not an icon problem. - **Pre-Release pill** replaces "Version 1.0.0" (the assembly version stays in log lines). The pill is a button: it opens llamacasty.com until a Steam store page exists, so the link changes in one place. - Tagline: "1-2-3, and go live to the world on YouTube!" The old "simple enough that even the most right-brained person can intuit it" is gone, and a test asserts it stays gone. - The offer in three short blocks (free 30 fps · paid 60 fps · one-time, lifetime, no account / subscription / tracking), then the launch price and **one primary CTA**: "Unlock 60 fps — $79". - Price: $79 until **April 1, 2027**, then $99 locked for life. The numbers live once, in `MainViewModel.Overlays`, and are recorded in `docs/ai/commercial.md`. - **A paying customer is never sold to**: ownership collapses the price/CTA block and shows "Licence: owned via Steam — thank you." - Secondary destinations (channel, Mastodon, coffee, licences) are one quiet row; footer is `© Llama Chile Shop`; the "Made by gramps" line is removed. - `YtButtonSecondary` now carries a higher surface, a hairline border and a keyboard focus ring — they were the same tone as the panel behind them, which is why they read as plain text. Test: `AboutHubTests.About_ShowsTheOfferToFreeUsers_AndNothingToSellToOwners` — tagline, pill, price, CTA text, copyright, the removed lines stay removed, and the CTA block collapses when owned. FAST gate: 540 passed, 0 failed, 0 warnings.
This commit is contained in:
@@ -75,6 +75,69 @@ public sealed class AboutHubTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The About box is a conversion surface (creator, 2026-11-05): a large logo, the 1-2-3 tagline,
|
||||
/// the offer, one primary CTA with the price on it — and a paying customer sees none of the price.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void About_ShowsTheOfferToFreeUsers_AndNothingToSellToOwners()
|
||||
{
|
||||
_app.Run(() =>
|
||||
{
|
||||
var tempDb = Path.Combine(Path.GetTempPath(), $"llamacasty-about-{Guid.NewGuid():N}.db");
|
||||
MainViewModel.LayoutPathOverride = tempDb;
|
||||
MainWindow? window = null;
|
||||
try
|
||||
{
|
||||
window = new MainWindow();
|
||||
var vm = (MainViewModel)window.DataContext;
|
||||
vm.EntitlementOverride = new FakeEntitlement(false, "Steam licensing is not configured in this build.");
|
||||
window.Show();
|
||||
vm.OpenAboutCommand.Execute(null);
|
||||
window.UpdateLayout();
|
||||
|
||||
var badge = FindVisualChildByName<Button>(window, "ReleaseBadgeButton");
|
||||
Assert.NotNull(badge);
|
||||
Assert.Equal("Pre-Release", badge!.Content);
|
||||
Assert.NotNull(badge.Command); // the badge links out until the Steam page exists
|
||||
|
||||
var tagline = FindVisualChildByName<TextBlock>(window, "AboutTagline");
|
||||
Assert.NotNull(tagline);
|
||||
Assert.Equal("1-2-3, and go live to the world on YouTube!", tagline!.Text);
|
||||
// The old line insulted the creator and is gone for good.
|
||||
Assert.DoesNotContain("right-brained", TextOf(window));
|
||||
|
||||
var price = FindVisualChildByName<TextBlock>(window, "AboutPriceLine");
|
||||
Assert.Equal("Launch price $79", price!.Text);
|
||||
|
||||
var cta = FindVisualChildByName<Button>(window, "UpgradeCtaButton");
|
||||
Assert.NotNull(cta);
|
||||
Assert.Equal("Unlock 60 fps — $79", cta!.Content);
|
||||
var upgradeBlock = FindVisualChildByName<StackPanel>(window, "UpgradeBlock");
|
||||
Assert.Equal(Visibility.Visible, upgradeBlock!.Visibility);
|
||||
|
||||
var copyright = FindVisualChildByName<TextBlock>(window, "AboutCopyright");
|
||||
Assert.Equal("© Llama Chile Shop", copyright!.Text);
|
||||
Assert.DoesNotContain("Made by gramps", TextOf(window));
|
||||
|
||||
// A paying customer is never sold to.
|
||||
vm.EntitlementOverride = new FakeEntitlement(true);
|
||||
window.UpdateLayout();
|
||||
Assert.Equal(Visibility.Collapsed, FindVisualChildByName<StackPanel>(window, "UpgradeBlock")!.Visibility);
|
||||
var owned = FindVisualChildByName<TextBlock>(window, "OwnedStateText");
|
||||
Assert.Equal(Visibility.Visible, owned!.Visibility);
|
||||
Assert.Contains("owned", owned.Text, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
finally
|
||||
{
|
||||
window?.Close();
|
||||
MainViewModel.LayoutPathOverride = null;
|
||||
SqliteConnection.ClearAllPools();
|
||||
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Free_user_gets_1080p30_and_cannot_select_or_change_to_60fps()
|
||||
{
|
||||
@@ -154,6 +217,30 @@ public sealed class AboutHubTests
|
||||
});
|
||||
}
|
||||
|
||||
private static T? FindVisualChildByName<T>(DependencyObject parent, string name) where T : DependencyObject
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is FrameworkElement element && element.Name == name && child is T typed) return typed;
|
||||
if (FindVisualChildByName<T>(child, name) is { } found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Every TextBlock string in the tree — for asserting that removed copy is really gone.</summary>
|
||||
private static string TextOf(DependencyObject parent)
|
||||
{
|
||||
var text = new System.Text.StringBuilder();
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(parent, i);
|
||||
if (child is TextBlock block) text.Append(block.Text).Append('\n');
|
||||
text.Append(TextOf(child));
|
||||
}
|
||||
return text.ToString();
|
||||
}
|
||||
|
||||
private static T? FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
|
||||
{
|
||||
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
|
||||
|
||||
Reference in New Issue
Block a user