Skip to content

feat: add "Get the app" header entry point with QR popover - #6384

Open
tsahimatsliah wants to merge 9 commits into
mainfrom
claude/app-promotion-button-4e840d
Open

feat: add "Get the app" header entry point with QR popover#6384
tsahimatsliah wants to merge 9 commits into
mainfrom
claude/app-promotion-button-4e840d

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 28, 2026

Copy link
Copy Markdown
Member

Changes

Adds a "Get the app" entry point to the desktop header, promoting the daily.dev mobile app to webapp and extension visitors. Today the only pointer lives buried in the profile menu.

  • New GetAppButton (features/getApp) mounted in HeaderButtons for both the logged-out and logged-in headers.
    • Logged out: labelled pill ahead of the Log in / Sign up pair. Stays Float, never Primary, so it does not compete with Sign up.
    • Logged in: icon-only, one slot left of the notifications bell, with the label in a tooltip. That rail already carries five items, so a label would blow it out.
  • Popover with a QR code, plus App Store and Google Play links as a fallback. The QR leads because a desktop visitor cannot install a phone app from the machine they are on — a bare store link is a dead end there.
  • One-time attention dot on the icon, cleared the first time the panel is opened and persisted via usePersistentContext. It nudges once and then goes quiet — deliberately not a recurring banner.
  • Storybook page at Components → GetAppButton (5 stories, both themes) so the design is reviewable without touching GrowthBook.

Placement rationale

Looked at how other products do this before choosing. TikTok ships an icon-only phone button (tooltip "Get App") in its header action rail next to the auth buttons; Spotify's web player uses a labelled "Open App". Both are persistent header affordances that open a QR, not banners.

Rejected alternatives:

  • Top-of-page banner — that slot is already used by post-signup activation, and a banner is one-shot and dismissible, which is the wrong shape for permanent awareness.
  • Sidebar / profile menu — "Get the mobile app" already lives there and clearly is not doing the job. That is the status quo.

QR implementation notes

  • The matrix is pre-rendered and committed, not generated at runtime — the target URL never changes, so a QR library in the header bundle is not worth it. Regeneration command is documented in the file.
  • Error correction level H specifically so the centre can be knocked out for the daily.dev mark. At level M the branded version would not reliably scan.
  • Encodes https://daily.dev/apps?utm_source=header_qr. A single UTM param keeps the code at 37 modules; the two-param version pushed it to 45 and got noticeably denser.
  • Verified it actually scans rather than by eye: rendered through a real browser and decoded the pixels with jsQR at 96 / 112 / 128 / 160px. All decode to the exact expected URL.
  • The QR plate stays white in both themes so scanners keep their contrast; the badge is fixed dark-with-white-mark for the same reason.

Desktop-only by design

Gated on useViewSize(ViewSize.Laptop) rather than platform detection. The native iOS/Android wrappers render this same webapp shell, so a viewport gate is what actually keeps us from telling someone holding the app to go install the app.

Events

Type event_name value
New click target_type: 'get app button' — fired when the popover opens
New download app target_type: 'get app button', target_id: 'ios' | 'android' — fired on a store link click

Both reuse existing LogEvent members. Adds one new TargetType.GetAppButton. Analytics Taxonomy sheet still needs updating.

Experiment

No — this is not an A/B experiment and no #experiments enrollment is needed. It's a permanent entry point with no hypothesis or variant analysis planned. header_get_app is a plain GrowthBook feature flag used as a rollout kill-switch: default false in code, created in GrowthBook and ramped to 100% when we're ready, instantly rampable back down if anything looks off.

Note

The branch briefly carried a temp commit (e35b4fae4) forcing the default to true so the entry point rendered on the preview deploy (GrowthBook DevTools can't attach on preview builds — they build as production). That commit has been reverted in 92324b6dd; the committed default is false again, so the merged code is inert until the flag is created and ramped in GrowthBook.

Manual Testing

On those affected packages:

  • Have you done sanity checks in the webapp? — via Storybook only, see caveat below
  • Have you done sanity checks in the extension?
  • Does this not break anything in companion?

Did you test the modified components media queries?

  • MobileL (420px) — verified the button correctly renders nothing
  • Tablet (656px) — verified the button correctly renders nothing
  • Laptop (1020px) — full behaviour, both themes

Did you test on actual mobile devices?

  • iOS (Chrome and Safari)
  • Android

Caveat for the reviewer: verification was done in Storybook (which forces the flag on via an isFeatureEnabled prop) and through automated browser checks, not in a running webapp or extension with the flag flipped in GrowthBook. Worth a pass there before enrolment. Native mobile is not applicable — the component is gated off below laptop width — but that gate itself is worth confirming on a real device.

Automated

  • 18 tests pass across features/getApp and components/layout (6 new specs: flag off, below-laptop, pre-resolved flag, both store hrefs, label variant)
  • ESLint clean, scripts/typecheck-strict-changed.js clean
  • Webapp suite green apart from the known-flaky MyFeedPage › should request user feed, which passes on re-run

Two bugs found and fixed while verifying

  • Popover copy overflowed the panel: the global reset pins flex-shrink: 0, so min-w-0 alone never lets a flex child wrap — it needs flex-1 too.
  • The header icon sat 4px off-centre whenever the dot was showing: Button derives its icon-only geometry from !children, so nesting the dot inside silently switched it to the labelled layout (pl-2 pr-4 gap-1). The dot is now a sibling.

Open question

The logged-out row is currently [Get the app] [Log in] [Sign up] — three text buttons, and "Get the app" is the widest. It pulls attention away from Sign up, which is the CTA that matters to a logged-out visitor. InHeaderLoggedOutCompact in Storybook shows the icon-only alternative against the same pair; dropping showLabel in HeaderButtons is a one-line switch if reviewers prefer it.

🤖 Generated with Claude Code

Preview domain

https://claude-app-promotion-button-4e84.preview.app.daily.dev

Desktop and extension visitors have no visible path to the mobile app - the
only pointer lives buried in the profile menu. This adds a phone button to
the header that opens a popover with a scannable QR code plus App Store and
Google Play links.

A desktop visitor cannot install a phone app from the machine they are on,
so the QR is the primary affordance and the store links are the fallback.
The code is a pre-rendered matrix rather than a runtime library, generated
at error-correction level H so the centre can carry the daily.dev mark
without becoming unscannable.

Gated behind the header_get_app flag, default false. Rendered only at laptop
width and up: the native iOS/Android wrappers run this same webapp shell and
should not be told to install an app they are already running.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Jul 29, 2026 12:39pm
storybook Building Building Preview Jul 29, 2026 12:39pm

Request Review

@capJavert capJavert left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No code-level blocker found in the first pass, but this is not ready for final approval yet: the PR explicitly says experiment enrollment has not been requested and the component has only been verified in Storybook, not in a running webapp/extension with the GrowthBook flag. Please complete enrollment and the stated webapp + extension viewport sanity pass, then request final approval.

REVERT BEFORE MERGE.

GrowthBook has no `header_get_app` feature yet, and the DevTools extension
cannot attach on preview deploys because they build as production. Flipping
the committed default is the only way to see the entry point on the preview.

Merging this as-is ships the experiment to 100% of users with no rollback
short of a deploy. Restore `new Feature('header_get_app', false)`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The App Store / Google Play links were Secondary outlines, which read as
tertiary next to the QR. They are the fallback path for anyone who cannot
scan, so they get the solid Primary fill and the Medium size instead.

Both icons draw in currentColor, so they invert with the fill and stay
legible in either theme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah

Copy link
Copy Markdown
Member Author

@capJavert thanks — fair call on both points. Here's where each stands.

✅ Webapp sanity pass — done, on the preview deploy

The flag is now forced on via e35b4fae4 (temp commit, see the caution block at the top of the description), so the entry point actually renders on the preview and I could exercise it for real rather than only in Storybook.

Verified on https://claude-app-promotion-button-4e84.preview.app.daily.dev/tags/javascript (logged out):

  • Renders in the real header as [Get the app] [Log in] [Sign up]
  • Popover opens; QR renders; both store links carry the right hrefs with target="_blank" rel="noopener"
  • No console errors from this component (only pre-existing [GSI_LOGGER] FedCM noise from Google sign-in)

Viewport matrix

Width Trigger Horizontal overflow
420 (MobileL) absent ✅ none
656 (Tablet) absent ✅ none
1020 (Laptop) present, 140×40 ✅ none
1440 present ✅ none

Absent below laptop is intended — the native iOS/Android wrappers render this same webapp shell, so the gate is what stops us telling someone holding the app to go install the app. Panel measures 384px and sits fully inside the viewport at 1020 (left: 428, right: 812).

One thing I found that is not a regression from this PR

Resizing from 1020 → 900 without a reload leaves the button mounted even though matchMedia('(min-width: 1020px)') has flipped to false.

This is pre-existing and header-wide, not something this PR introduces: at 900px after a live resize the whole desktop <header> is still mounted, Log in and Sign up included, when MainLayoutHeader should have swapped to FeedNav. On a fresh load at any width everything is correct. Flagging it in case it's news, but it looks orthogonal to this change.

⚠️ Extension — built and tested, not click-tested

  • pnpm --filter extension build compiles clean (chrome.zip produced), so nothing about the new shared component breaks extension bundling
  • Extension suite green: 6 suites, 43 tests

I have not loaded the unpacked extension and clicked through the new-tab header, and I did not verify the logged-in variant on the preview since that needs a real session. Both are still open if you want them covered before approval — happy to, or grab them on your side if that's quicker.

❌ Experiment enrollment — still outstanding, needs someone with access

header_get_app does not exist in GrowthBook yet, and the #experiments post has not gone out. I can't do either from here, so this one genuinely needs a human. This should not merge until enrollment is approved.

Worth knowing for whoever picks it up: the GrowthBook DevTools extension cannot force this flag on a preview deploy — enableDevMode is gated on !isProduction and previews build as production. That's why the temp default flip exists. It works fine against a local dev server.

🔴 Before merge

Revert e35b4fae4 so the flag reads new Feature('header_get_app', false). Merging the forced default ships the experiment to 100% of users with no rollback short of a deploy.

Also changed since your review

b5ab18802 — the App Store / Google Play links moved from Secondary outlines to solid Primary buttons at Medium size. They're the fallback path for anyone who can't scan, and as outlines they read as tertiary next to the QR. Both icons draw in currentColor so they invert with the fill; checked in both themes.

Still open in the description: the logged-out row is three text buttons now, and "Get the app" is the widest of them, competing with Sign up. InHeaderLoggedOutCompact in Storybook shows the icon-only alternative against the same pair — dropping showLabel is a one-line switch if you'd prefer it.

The Android robot is the OS logo, not the storefront, so it was the wrong
mark next to "Google Play". Adds the official Play glyph as a local icon
drawn in currentColor, matching how AndroidIcon was built.

The two icons also read at different sizes because their artwork fills the
viewBox differently: the Apple glyph spans ~67% of its box while the Play
mark runs nearly edge to edge. At one shared size the Apple logo had 56%
less ink area. Sizing Apple's box up to 20px against Play's 16px lands them
within 4% of each other (ink 13.2x16.7 vs 14.4x16.0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every header child is flex-shrink:0 via the global reset, so once the
logged-in action rail outgrows its budget the header overflows instead of
compressing: at 1020px the rail already overshoots the right edge by ~27px
today and clips the logo on the left, and the new Get-the-app entry adds
another 52px.

The search is the one element that can give up width, so it now takes the
remaining space (flex-1 + min-w-0) and its trigger caps at 26.25rem rather
than being fixed there. Rendered width is unchanged at 420px wherever there
is room - it only shrinks when the header would otherwise overflow.

The laptop/laptopL max-widths removed from the trigger were dead: the fixed
width never reached them.

Measured against the real header with the rail reconstructed at true sizes:
overshoot goes to 0 at 1020/1100/1280/1440 for profile clusters of
180/270/320px, and the logo is no longer clipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@capJavert capJavert left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking before merge: featureHeaderGetApp still defaults to true because temporary commit e35b4fa remains in the branch. As the PR notes, this bypasses the intended GrowthBook rollout and ships the entry point to everyone. Revert that commit/default to false, complete experiment enrollment, and finish the logged-in extension sanity pass before requesting final approval.

@tsahimatsliah

Copy link
Copy Markdown
Member Author

@capJavert addressing your three blockers — two are resolved, one has product context that changes it.

✅ 1. Temp commit — reverted

92324b6dd reverts e35b4fae4; the committed default is back to new Feature('header_get_app', false) and no trace of the forced default remains in the branch. The caution block is gone from the description. (Side effect: the preview deploy no longer renders the button — that flip was the only way to see it there, since GrowthBook DevTools can't attach on preview builds. The webapp sanity pass it enabled is done and documented above, so it served its purpose.)

✅ 2. Experiment enrollment — not applicable, this isn't an experiment

Product decision: we're not running this as an A/B test. There's no hypothesis to measure and no variant analysis planned — it's a permanent entry point, like the store links in the sidebar. header_get_app stays as a plain GrowthBook feature flag acting as a rollout kill-switch:

  • default false in code (restored above), so merging ships nothing
  • created in GrowthBook and ramped to 100% when we're ready
  • instantly rampable back down if anything looks off — no deploy needed

So there's nothing to enroll in #experiments. I've updated the PR description's Experiment section to say exactly this, so the paper trail matches.

⚠️ 3. Logged-in extension pass — open, but now zero-risk to defer

With the default back to false and no header_get_app feature existing in GrowthBook yet, the merged code is inert on every surface, extension included — flag-off renders null, the extension build compiles clean, and its suite is green (6 suites, 43 tests). The remaining click-through of the logged-in new-tab header can happen while the flag ramps, before any extension user can see the button. If you'd rather have it strictly pre-merge anyway, say the word and we'll grab it.

Given 1 and 2, requesting re-review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants