diff --git a/.env.example b/.env.example index 4abf786f..15079a98 100644 --- a/.env.example +++ b/.env.example @@ -86,13 +86,9 @@ EXPO_PUBLIC_API_URL= # --- App funnel visibility (optional; website) --- # Per-store launch flags gate the public /app funnel. /app is visible (download # mode) when either is true, and redirects home (no nav/homepage links) when both -# are false. iOS launches first; Android trails through its Closed Test. -# -# iOS App Store listing is public (default true — iOS is approved today). +# are false. Set one to 'false' to pull that store's download CTA. PUBLIC_APP_IOS_LAUNCHED=true -# Google Play listing is public. Set 'true' once Google grants production access; -# until then the Android tile routes to /android-testers instead of a Play badge. -PUBLIC_APP_ANDROID_LAUNCHED=false +PUBLIC_APP_ANDROID_LAUNCHED=true # --- Answer recording opt-in (optional) --- # Recording feeds the shared Global progress and is ALWAYS on in production builds. diff --git a/apps/website/netlify.toml b/apps/website/netlify.toml index e048d88b..fd0bee43 100644 --- a/apps/website/netlify.toml +++ b/apps/website/netlify.toml @@ -47,3 +47,16 @@ # [plugin.inputs] # enable = false + +# The Android Closed Test is over; send old tester links to the public download page. +[[redirects]] + from = "/android-testers" + to = "/app" + status = 302 + force = true + +[[redirects]] + from = "/android-testers-birthday-present" + to = "/app" + status = 302 + force = true diff --git a/apps/website/playwright.ssr.config.ts b/apps/website/playwright.ssr.config.ts index 2a333ead..fc35c352 100644 --- a/apps/website/playwright.ssr.config.ts +++ b/apps/website/playwright.ssr.config.ts @@ -33,6 +33,6 @@ export default defineConfig({ reuseExistingServer: !process.env.CI, timeout: 120_000, // /app renders in download mode under the default flags (PUBLIC_APP_IOS_LAUNCHED - // defaults on, Android off), which is exactly what the SSR /app suite asserts. + // and PUBLIC_APP_ANDROID_LAUNCHED default on), which is what the SSR /app suite asserts. }, }) diff --git a/apps/website/public/og/android-testers-birthday-present.png b/apps/website/public/og/android-testers-birthday-present.png deleted file mode 100644 index 7fd3a3ca..00000000 Binary files a/apps/website/public/og/android-testers-birthday-present.png and /dev/null differ diff --git a/apps/website/src/constants/app-visibility.ts b/apps/website/src/constants/app-visibility.ts index 5aa0007d..b0c85083 100644 --- a/apps/website/src/constants/app-visibility.ts +++ b/apps/website/src/constants/app-visibility.ts @@ -2,12 +2,11 @@ * Pure visibility rule for the public /app funnel, kept free of any `env` import * so it can be unit-tested without the full environment. * - * iOS and Android launch on separate timelines (iOS is public; Android trails by - * Google's Closed Test), so the flags are per-store. The funnel (route, nav entry, + * iOS and Android launched on separate timelines, so the flags are per-store. The funnel (route, nav entry, * homepage CTA) is reachable as soon as either store is live: * * PUBLIC_APP_IOS_LAUNCHED — iOS App Store listing is live (default on) - * PUBLIC_APP_ANDROID_LAUNCHED — Google Play listing is live (default off) + * PUBLIC_APP_ANDROID_LAUNCHED — Google Play listing is live (default on) * * ios=false, android=false → hidden (/app redirects home) * ios=true OR android=true → visible (download mode) diff --git a/apps/website/src/constants/app.ts b/apps/website/src/constants/app.ts index 185d3023..c6c76f05 100644 --- a/apps/website/src/constants/app.ts +++ b/apps/website/src/constants/app.ts @@ -3,8 +3,7 @@ * * iOS and Android launch on separate timelines, so each store has its own flag: * - PUBLIC_APP_IOS_LAUNCHED iOS App Store listing is public (default true). - * - PUBLIC_APP_ANDROID_LAUNCHED Google Play listing is public (default false — - * Android is still in Google's mandatory Closed Test). + * - PUBLIC_APP_ANDROID_LAUNCHED Google Play listing is public (default true). * * While Android is in testing, /app offers the real iOS download and routes * Android visitors to the /android-testers funnel instead of a Play badge. @@ -25,9 +24,8 @@ import {computeAppVisible} from './app-visibility' export const APP_IOS_LAUNCHED: boolean = env.PUBLIC_APP_IOS_LAUNCHED /** - * True once the Google Play listing is public. Flip via PUBLIC_APP_ANDROID_LAUNCHED - * after Google grants production access. Until then Android visitors are sent to - * the /android-testers Closed Test funnel. + * True when the Google Play listing is public. When false, Android visitors are + * sent to the /android-testers Closed Test funnel. */ export const APP_ANDROID_LAUNCHED: boolean = env.PUBLIC_APP_ANDROID_LAUNCHED diff --git a/apps/website/src/pages/android-testers-birthday-present.astro b/apps/website/src/pages/android-testers-birthday-present.astro deleted file mode 100644 index 3303481f..00000000 --- a/apps/website/src/pages/android-testers-birthday-present.astro +++ /dev/null @@ -1,185 +0,0 @@ ---- -import {Icon} from 'astro-icon/components' - -import Button from '~components/ui/Button.astro' -import { - ANDROID_TESTER_GROUP_URL, - ANDROID_TESTER_LINKS_READY, - ANDROID_TESTER_OPT_IN_URL, -} from '~constants/android-testers' -import Layout from '~layouts/Layout.astro' - -// Kept as a separate page so the personal birthday copy can change independently -// from the evergreen Android tester instructions. -const steps = [ - { - title: 'Join the group', - body: 'Use the Google account on your Android phone.', - }, - { - title: 'Open the Play test', - body: 'Tap “Become a tester”, then install WhoCards.', - }, - { - title: 'Play for a minute', - body: 'Draw a few Cards. Leave the app installed for now.', - }, - { - title: 'Send me one Question', - body: 'Pick one you like and message it back to me.', - }, -] ---- - - -
-
-
-

- - Birthday ask -

- -

- For my birthday, can I ask you one good Question? -

- -

- If you have an Android phone, this takes five minutes: join the test, install the app, - play one tiny round, and send me one Question you like. -

- -

- I keep thinking being human is mostly learning how to ask a better question. Help me put a - few more good ones into the world. -

- - { - ANDROID_TESTER_LINKS_READY ? ( -
- - -
- ) : ( -
-

Testing links open soon.

-

- I am preparing the Google Group and Play testing link. Check back here before - installing. -

-
- ) - } - -

- Same Google account for both links. Then send me one Question you like. -

-
- -
-
-
-
- -
-
-
-

- Why this helps -

-

- Google needs real testers before launch. -

-
-

- Android makes small apps prove they have real testers. You joining helps WhoCards get to the - Play Store. -

-
-
-
diff --git a/apps/website/src/pages/app.astro b/apps/website/src/pages/app.astro index c883c9b1..46a8c910 100644 --- a/apps/website/src/pages/app.astro +++ b/apps/website/src/pages/app.astro @@ -110,7 +110,9 @@ const features = [
{ it.each([ '/404', '/android-testers', - '/android-testers-birthday-present', '/images', '/hu/images', '/dev/image-playground', @@ -52,7 +51,6 @@ describe('search indexation policy', () => { it.each([ '/android-testers.html', - '/android-testers-birthday-present.html', '/events/hajnalig/play.html', '/events/hajnalig/2025/play.html', '/events/sdd-2026.html', diff --git a/apps/website/src/seo/indexation.ts b/apps/website/src/seo/indexation.ts index 7392941b..ab46dd23 100644 --- a/apps/website/src/seo/indexation.ts +++ b/apps/website/src/seo/indexation.ts @@ -15,7 +15,6 @@ export function isNoindexRoute(pathname: string): boolean { return ( route === '/404' || route === '/android-testers' || - route === '/android-testers-birthday-present' || route.endsWith('/images') || route === '/events/hajnalig/play' || /^\/events\/hajnalig\/[^/]+\/play$/.test(route) || diff --git a/apps/website/tests/e2e-ssr/app.spec.ts b/apps/website/tests/e2e-ssr/app.spec.ts index 03b8a9a6..29a8ce43 100644 --- a/apps/website/tests/e2e-ssr/app.spec.ts +++ b/apps/website/tests/e2e-ssr/app.spec.ts @@ -1,13 +1,12 @@ import {expect, test} from '@playwright/test' // /app is SSR (prerender = false). The e2e-ssr suite runs against `astro dev` -// under the default flags (PUBLIC_APP_IOS_LAUNCHED on, PUBLIC_APP_ANDROID_LAUNCHED -// off) → the page renders in download mode: iOS is a real App Store download and -// Android routes to the /android-testers Closed Test funnel. These assertions need -// no external services (no valid form submit, so neither Postgres nor Resend is hit). +// under the default flags (both stores launched) → the page offers both store +// downloads. These assertions need no external services (no valid form submit, +// so neither Postgres nor Resend is hit). -test.describe('/app download mode (iOS live, Android in testing)', () => { - test('offers the real iOS App Store download', async ({page}) => { +test.describe('/app download mode (iOS + Android live)', () => { + test('offers both store downloads', async ({page}) => { const res = await page.goto('/app') // Prove we are on /app, not an unexpected redirect to /. @@ -16,35 +15,20 @@ test.describe('/app download mode (iOS live, Android in testing)', () => { const html = await res!.text() expect(html).toContain('apps.apple.com') + expect(html).toContain('play.google.com') await expect(page.locator('a[data-store="ios"]').first()).toBeVisible() + await expect(page.locator('a[data-store="android"]').first()).toBeVisible() + await expect(page.locator('a[data-store="android-test"]')).toHaveCount(0) }) - test('routes Android visitors to the tester funnel, not a dead Play badge', async ({page}) => { - const res = await page.goto('/app') - const html = await res!.text() - - // While Android is in Closed Testing the Play store link must not appear. - expect(html).not.toContain('play.google.com') - - // The android-test tile is the tester-funnel route (its href is the configured - // ANDROID_TESTER_SIGNUP_URL, which may be overridden per environment, so we only - // assert the tile is present rather than a hardcoded path). - await expect(page.locator('a[data-store="android-test"]').first()).toBeVisible() - }) - - test('leads Android visitors with the tester tile (UA-aware ordering)', async ({browser}) => { - // Exercise the server-side UA branch: an Android visitor's first tile must be - // the tester route, not the iOS App Store badge they can't use. + test('leads Android visitors with Google Play (UA-aware ordering)', async ({browser}) => { const context = await browser.newContext({ userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 Chrome/124 Mobile', }) const page = await context.newPage() await page.goto('/app') - await expect(page.locator('a[data-store]').first()).toHaveAttribute( - 'data-store', - 'android-test' - ) + await expect(page.locator('a[data-store]').first()).toHaveAttribute('data-store', 'android') await context.close() }) diff --git a/apps/website/tests/e2e/smoke.spec.ts b/apps/website/tests/e2e/smoke.spec.ts index 16e0cd7c..ab4d457b 100644 --- a/apps/website/tests/e2e/smoke.spec.ts +++ b/apps/website/tests/e2e/smoke.spec.ts @@ -28,9 +28,7 @@ test('AI Check-In has intent-aligned metadata and links back to the conversation }) => { await page.goto('/ai-at-work') - await expect(page).toHaveTitle( - 'WhoCards | AI Team Check-In Questions for Honest Conversations' - ) + await expect(page).toHaveTitle('WhoCards | AI Team Check-In Questions for Honest Conversations') await expect(page.locator('meta[name="description"]')).toHaveAttribute( 'content', /Talk to your team about AI with a free set of AI team check-in questions/ @@ -70,12 +68,7 @@ test('unknown route serves the 404 page', async ({request}) => { }) test('production HTML aligns robots metadata with temporary route policy', async ({page}) => { - for (const route of [ - '/android-testers', - '/android-testers-birthday-present', - '/events/hajnalig/play', - '/events/hajnalig/2025/play', - ]) { + for (const route of ['/android-testers', '/events/hajnalig/play', '/events/hajnalig/2025/play']) { await page.goto(route) await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', 'noindex, follow') } diff --git a/docs/DESIGN.md b/docs/DESIGN.md index c0b7a178..67ee9ef5 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -142,18 +142,14 @@ For the first mobile release, this specification is subordinate to `docs/RELEASE - The v1 journey is landing → the single WhoCards Deck → Global Game. Do not imply that Library browsing, accounts, purchases, Custom Decks, Personal Game, or Facilitation already exist. - `/app` is gated by `APP_VISIBLE` — visible whenever a store is live - (`PUBLIC_APP_IOS_LAUNCHED` / `PUBLIC_APP_ANDROID_LAUNCHED`); with both off it redirects home. iOS - launches first (flag default **on**); Android trails through its Closed Test (flag default - **off**), so the Android tile routes to `/android-testers` until Google grants production access. + (`PUBLIC_APP_IOS_LAUNCHED` / `PUBLIC_APP_ANDROID_LAUNCHED`, both default **on**); with both off it + redirects home. With the Android flag off, the Android tile routes to `/android-testers`. Each platform's store link is shown only once its flag is on — never show a placeholder store link or claim availability early. - App-notification and newsletter consent are separate. Newsletter consent is optional, unchecked, and described plainly; confirmation copy must reflect exactly what the person selected. -- iOS and Android launch on separate timelines: iOS is public now, Android follows after its Closed - Test. The Android tile leads to `/android-testers` (not a Play badge) until `PUBLIC_APP_ANDROID_LAUNCHED` - flips. Don't imply Android is downloadable before then, and don't hold the approved iOS build for - it. Each public binary completes a ~24-hour quiet soak before that platform's CTAs and announcement - go live. +- iOS and Android are both public. Each public binary completes a ~24-hour quiet soak before that + platform's announcement goes out. - Optimize every acquisition surface for the next meaningful step: `/app` → signup or store click; install → first play. Keep store, email, homepage, and in-product CTA wording consistent. - The native review request may appear only after the documented happy-moment eligibility gate. diff --git a/packages/env/src/website.ts b/packages/env/src/website.ts index 2f3b34de..31528e7a 100644 --- a/packages/env/src/website.ts +++ b/packages/env/src/website.ts @@ -33,7 +33,7 @@ export function createWebsiteEnv(runtimeEnv: RuntimeEnv) { // Per-store switches: /app offers a real download for a launched store and // routes the other into its closed test (Android: /android-testers). PUBLIC_APP_IOS_LAUNCHED: z.stringbool().default(true), - PUBLIC_APP_ANDROID_LAUNCHED: z.stringbool().default(false), + PUBLIC_APP_ANDROID_LAUNCHED: z.stringbool().default(true), // Use Cloudflare's always-pass test keys locally. PUBLIC_TURNSTILE_SITE_KEY: z.string().min(1), },