From 0c61dc32ee7bf49acffd60ee1e6bf7b6a486807d Mon Sep 17 00:00:00 2001 From: TaprootFreakAI <315477232+TaprootFreakAI@users.noreply.github.com> Date: Wed, 9 Sep 2026 03:00:28 +0200 Subject: [PATCH 1/8] fix(invite): keep English copy english and explain a JS-less landing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups from the review of #30. ?lang=en without a code flipped html lang and og:locale to English while the shell text stayed German — crawlers snapshot exactly those bytes. shareTitle and shareDescription now fall back to generic English copy. Without JavaScript the loading state sat there for ever: nothing resolves the code. Both shells now hide it and say why, and a Playwright case with javaScriptEnabled:false asserts the served bytes. --- functions/lib/itunes-banner.js | 12 ++++++++++-- public/invite/index.html | 16 ++++++++++++++++ public/promo/index.html | 16 ++++++++++++++++ test/itunes-banner-function.test.mjs | 20 ++++++++++++++++++++ tests/behavior.spec.mjs | 18 ++++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/functions/lib/itunes-banner.js b/functions/lib/itunes-banner.js index b18c0b1..686ea04 100644 --- a/functions/lib/itunes-banner.js +++ b/functions/lib/itunes-banner.js @@ -426,7 +426,14 @@ export function parseLangFromUrl(urlLike) { /** Crawlers snapshot og:title / twitter:title from the HTML bytes. Names wait for lookup JS. */ export function shareTitle(kind, code, lang) { - if (!kind || !code) return null; + // Without a code there is nothing code-specific to say, but ?lang=en still + // flips html lang and og:locale — leaving the German shell text under an + // English locale for crawlers. Fall back to a generic English title. + if (!code) { + if (lang !== 'en') return null; + return kind === 'promo' ? 'RealUnit — Promo code' : 'RealUnit — Invitation'; + } + if (!kind) return null; if (lang === 'en') { return kind === 'promo' ? 'RealUnit — Promo code ' + code : 'RealUnit — Invitation ' + code; } @@ -477,7 +484,8 @@ export function injectShareImageAltHtml(html, kind, code, lang) { /** Crawlers snapshot og:description from the HTML bytes. Names wait for lookup JS. */ export function shareDescription(code, lang) { - if (!code) return null; + // Same reason as shareTitle: an English locale must not keep German copy. + if (!code) return lang === 'en' ? 'Open the RealUnit app with your code.' : null; if (lang === 'en') return 'Open the RealUnit app with code ' + code + '.'; return 'Öffne die RealUnit-App mit dem Code ' + code + '.'; } diff --git a/public/invite/index.html b/public/invite/index.html index d4c8943..93fdc74 100644 --- a/public/invite/index.html +++ b/public/invite/index.html @@ -150,6 +150,22 @@
+

Einladung wird geladen…

Einen Moment bitte.

diff --git a/public/promo/index.html b/public/promo/index.html index 1c0a5db..ee5b895 100644 --- a/public/promo/index.html +++ b/public/promo/index.html @@ -150,6 +150,22 @@
+

Promo-Code wird geladen…

Einen Moment bitte.

diff --git a/test/itunes-banner-function.test.mjs b/test/itunes-banner-function.test.mjs index 9edaddb..37cd86e 100644 --- a/test/itunes-banner-function.test.mjs +++ b/test/itunes-banner-function.test.mjs @@ -560,6 +560,26 @@ describe('referral code injection hardening', () => { }); }); +describe('an English locale without a code keeps English copy', () => { + test('shareTitle falls back to a generic English title', () => { + expect(shareTitle('invite', null, 'en')).toBe('RealUnit — Invitation'); + expect(shareTitle('promo', null, 'en')).toBe('RealUnit — Promo code'); + // German is the shell's own language, so there is nothing to replace. + expect(shareTitle('invite', null, 'de')).toBeNull(); + expect(shareTitle('invite', null, null)).toBeNull(); + }); + + test('shareDescription falls back to a generic English description', () => { + expect(shareDescription(null, 'en')).toBe('Open the RealUnit app with your code.'); + expect(shareDescription(null, 'de')).toBeNull(); + }); + + test('a code still wins over the fallback', () => { + expect(shareTitle('promo', 'EVT1', 'en')).toBe('RealUnit — Promo code EVT1'); + expect(shareDescription('EVT1', 'en')).toBe('Open the RealUnit app with code EVT1.'); + }); +}); + describe('paths the 100% gate now covers on the function module', () => { test('playStoreUrl without a code returns the bare store link', () => { expect(playStoreUrl(null, 'invite')).toBe(playStoreUrl(null, 'promo')); diff --git a/tests/behavior.spec.mjs b/tests/behavior.spec.mjs index 5b71939..b2f780b 100644 --- a/tests/behavior.spec.mjs +++ b/tests/behavior.spec.mjs @@ -694,6 +694,24 @@ test.describe('account-merge flow', () => { const REFERRAL_CODE_ENDPOINT = '**/v1/realunit/referral/code/**'; +test.describe('invite and promo landing without JavaScript', () => { + test.use({ javaScriptEnabled: false }); + + test('says why nothing resolves instead of spinning for ever', async ({ page }, testInfo) => { + test.skip(testInfo.project.name !== 'desktop-chromium', 'desktop-only check'); + for (const path of ['/invite/AB12CD', '/promo/EVT1']) { + await page.goto(path); + // Nothing can resolve the code, so the loading state must not be the + // only thing on screen. + await expect(page.locator('#state-loading')).toBeHidden(); + // Assert the served bytes: a no-JS visitor sees whatever the shell + // carries, and the hint lives in