From fff525986001c3e721905ee23c04d28ae12cbe72 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Thu, 24 Sep 2026 19:50:00 -0400 Subject: [PATCH] fix(brand): de-cake the actual source, not the file it is compiled over MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The About screen has been showing Cake Wallet's app icon, and the receive QR page — the screen a user screenshots to get paid — has been showing Cake's wordmark. In CI builds only, which is why nobody caught it. compile_graphics.sh compiles res/pictures/*.svg into assets/new-ui/*.svg.vec, and every platform workflow runs it. That makes res/pictures the source and assets/new-ui the output. The August de-cake pass replaced three files under assets/new-ui and left res/pictures untouched, so each CI build faithfully recompiled Cake's artwork straight over the top of ours. Locally it looked right, because locally nobody re-runs the compiler. Three files, all of them branding: hash_squircle_icon.svg, hash-wordmark.svg, hash-wordmark-light.svg. The sources are now the Hash Bags artwork, so the compiler produces what the filenames have claimed since August. This is the third time somebody else's artwork has reached users through this repo, and the second time an audit missed it, so it gets a test rather than another careful look: any SVG present in both res/pictures and assets/new-ui must be byte-identical. Drift means a hand-edit that the next CI build will silently revert. The test also asserts it compared something, because a silent zero is how the previous two got through. AGENTS.md has had the rule the whole time: generated files are never hand-edited, fix the source and regenerate. The trap here is that the generated copies sit beside the sources under identical names, and the ones the Dart code references are the generated ones. --- res/pictures/hash-wordmark-light.svg | 40 +++-------------- res/pictures/hash-wordmark.svg | 38 +++------------- res/pictures/hash_squircle_icon.svg | 41 +++-------------- test/brand_assets_match_source_test.dart | 57 ++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 99 deletions(-) create mode 100644 test/brand_assets_match_source_test.dart diff --git a/res/pictures/hash-wordmark-light.svg b/res/pictures/hash-wordmark-light.svg index 7d649c449..3a297c6ea 100644 --- a/res/pictures/hash-wordmark-light.svg +++ b/res/pictures/hash-wordmark-light.svg @@ -1,34 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +Hash Bags horizontal lockup +Hash Bags logo with green-boxed hash mark on the left and Hash Bags wordmark to the right. + +# +Hash Bags diff --git a/res/pictures/hash-wordmark.svg b/res/pictures/hash-wordmark.svg index 08d6e195b..161472a75 100644 --- a/res/pictures/hash-wordmark.svg +++ b/res/pictures/hash-wordmark.svg @@ -1,32 +1,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +Hash Bags horizontal lockup (dark theme) +Hash Bags logo with green-boxed hash mark on the left and a white wordmark to the right, intended for dark-theme backgrounds. + +# +Hash Bags diff --git a/res/pictures/hash_squircle_icon.svg b/res/pictures/hash_squircle_icon.svg index 8bafc44c4..602461007 100644 --- a/res/pictures/hash_squircle_icon.svg +++ b/res/pictures/hash_squircle_icon.svg @@ -1,36 +1,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + +Hash Bags app icon +Hash Bags app icon: brand-green rounded square with a centered white hash. + +# diff --git a/test/brand_assets_match_source_test.dart b/test/brand_assets_match_source_test.dart new file mode 100644 index 000000000..a6034687c --- /dev/null +++ b/test/brand_assets_match_source_test.dart @@ -0,0 +1,57 @@ +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; + +// compile_graphics.sh compiles res/pictures/*.svg into assets/new-ui/*.svg.vec, +// and CI runs it on every platform. That makes res/pictures the source and +// assets/new-ui the output, which is easy to get backwards: the files sit side +// by side, have identical names, and the ones under assets/ are the ones the +// Dart code names. +// +// Getting it backwards shipped Cake Wallet's artwork twice. The August de-cake +// pass replaced the copies under assets/new-ui and left res/pictures untouched, +// so every CI build recompiled Cake's icon over the top: their app icon on the +// About screen, and their wordmark on the receive QR page, which is the screen +// a user screenshots to get paid. A local build looked right, because locally +// nobody re-runs the compiler. +// +// So: any SVG present in both places must be byte-identical. If they drift, the +// one under assets/ is a hand-edit that the next CI build will silently revert. +void main() { + test('brand SVGs under assets/new-ui match their res/pictures source', () { + final assetsDir = Directory('assets/new-ui'); + final sourceDir = Directory('res/pictures'); + expect(assetsDir.existsSync(), isTrue, reason: 'no assets/new-ui — wrong CWD?'); + expect(sourceDir.existsSync(), isTrue, reason: 'no res/pictures — wrong CWD?'); + + final drifted = []; + var compared = 0; + for (final f in assetsDir.listSync().whereType()) { + if (!f.path.endsWith('.svg')) continue; + final name = f.uri.pathSegments.last; + final source = File('${sourceDir.path}/$name'); + if (!source.existsSync()) continue; // not compiled from res/pictures + compared++; + final a = f.readAsBytesSync(); + final b = source.readAsBytesSync(); + if (a.length != b.length || !_same(a, b)) { + drifted.add( + '$name: assets/new-ui is ${a.length}b, res/pictures is ${b.length}b. ' + 'Edit res/pictures and re-run ./compile_graphics.sh; the assets copy ' + 'is output and CI overwrites it.', + ); + } + } + + // A silent zero would make this test vacuous, which is how the last two + // branding regressions reached users. + expect(compared, greaterThan(0), reason: 'compared no SVG pairs at all'); + expect(drifted, isEmpty, reason: 'generated brand assets drifted from source:\n${drifted.join('\n')}'); + }); +} + +bool _same(List a, List b) { + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; +}