Skip to content

feat(onboarding): panel signup wall variant - #6376

Merged
tsahimatsliah merged 9 commits into
mainfrom
claude/signup-wall-panel
Jul 30, 2026
Merged

feat(onboarding): panel signup wall variant#6376
tsahimatsliah merged 9 commits into
mainfrom
claude/signup-wall-panel

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 26, 2026

Copy link
Copy Markdown
Member

Adds a panel background to the signup wall (FunnelStepType.HeroLanding): the form sits in a left column with the marketing landing page's hero artwork framed in the right one.

Selected by Freyja config via the step's background parameter — the default (cards) is untouched, so nothing changes until the parameter is set.

Rolling this out

There is no GrowthBook flag for this variant. The hero is a funnel step served by the API, and FunnelHeroLanding / OnboardingSignupHero / FunnelStepper contain no feature-flag calls — background: 'panel' can only arrive from the funnel JSON. The only GrowthBook flag on /onboarding is swipe_onboarding, which gates the EditTags step and is unrelated.

To run a 50/50, define two funnel variants in Freyja that differ only in the heroLanding step:

arm heroLanding.parameters
control omit background, or { "background": "cards" }
panel { "background": "panel" }

Preview a specific funnel before allocating traffic with /onboarding?id=<funnelId>&version=<version> — both params are forwarded straight to the boot call. Note /onboarding redirects to / on localhost, so this needs a deployed environment.

If we would rather own the split in GrowthBook than in Freyja, that is a small code change following the swipe_onboarding precedent — see this comment for the exact shape. Not included here.

Layout

Desktop (laptop+) — two columns. Form left, artwork right in a rounded frame with an "ambilight": a blurred, over-saturated copy of the artwork behind the frame, so the panel casts its own colour onto the page. A glass QR card floats bottom-right of the frame.

Stacked (below laptop) — the artwork takes the top of the screen and dissolves into the page background; the form is bottom-anchored underneath, matching the spacing of the existing cards/desk walls. The signup disclosure appears from tablet up and the footer links from laptop up, which is where the cards/desk walls put theirs.

Compact phones — a second tier keyed on viewport height (max-height: 759px, bounded to below laptop), because height is the axis under pressure: a 375×812 phone gets the roomy treatment, a 375×667 one does not. The artwork drops 50dvh → 32dvh and the type/gaps tighten with it.

Notes for review

  • The app-install card is a real link to https://r.daily.dev/get, daily.dev's smart link, so the destination is reachable by pointer, keyboard and voice control, not only by scanning. Its accessible name leads with the visible caption verbatim (Scan to get the app — daily.dev for iOS or Android) per WCAG 2.5.3, and the caption is a single shared constant so the two can't drift apart.
  • The same smart link routes iPhone → App Store, Android → Google Play, desktop → extension. Because the URL is fixed, the QR is a static asset: the module matrix was generated once at error-correction level H and baked in as a single SVG path. Payload independently decoded by @capJavert; a real-device camera scan through the centre logo badge is the one remaining unverified step.
  • Fixes a pre-existing bug in OnboardingRegistrationForm: tertiarySignupButtonClass hardcoded !text-white on a button sitting on the page background, making its label invisible in light mode. Now !text-text-primary. This affects every onboarding signup surface, not just this variant.
  • splitSignupStyle already existed and was fully plumbed but nothing switched it on. This variant enables it, which gives "Sign up with GitHub/Google" + "Create account". Its unused full-width "Sign in" button is removed in favour of the standard MemberAlready link.
  • A few onb-* class hooks in heroStyles.ts are set from OnboardingRegistrationForm (onb-split-cta, onb-split-login). They are inert wherever the hero's CSS is not present; both ends are commented.

Tests

Responsive visibility can't be asserted through jsdom, which does not evaluate media queries — an element hidden by a hidden class is still in the DOM. The specs therefore assert structurally: no hidden ancestor between the disclosure and the root, and conversely that the footer links do have one. Both were confirmed to fail when the fix is reverted.

Known gaps

  • No signup disclosure at phone widths, product-wide. OnboardingSignupHero's isMobile branch (below tablet, 656px) renders neither FooterLinks nor SignupDisclaimer, so the shipping cards wall shows no "By continuing, you agree to…" on a phone today — measured: 0 DOM matches at 390px, present at 700px. This variant matches that behaviour rather than diverging from it. The gap is real and pre-existing; fixing it belongs in its own change covering all three walls, since fixing it only here would leave cards/desk inconsistent.
  • className.onboardingSignup is dead across the auth stack: OnboardingRegistrationForm declares it and AuthOptionsInner forwards it, but the component never destructures className, so it has never applied for anyone passing it. Left alone here as out of scope.

Storybook

Components/Onboarding/Steps/FunnelHeroLandingPanel, plus Components/Onboarding/Signup wall comparison, which renders the current Cards/Desk walls next to Panel in real iframes at Mobile / MobileSmall / Tablet / Desktop sizes. Iframes rather than scaled divs because breakpoints come from the viewport in both CSS and useViewSize.

🤖 Generated with Claude Code

Preview domain

https://claude-signup-wall-panel.preview.app.daily.dev

@vercel

vercel Bot commented Jul 26, 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 30, 2026 8:44am
storybook Building Building Preview Jul 30, 2026 8:44am

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.

The panel variant still has an accessibility blocker before merge: the QR card is the only app-install destination, but the SVG is aria-hidden and there is no link a keyboard or screen-reader user can follow. Please add an accessible smart-link fallback (it can be visually hidden if needed) and cover it with a test. The real-device QR scan noted in the PR should also be completed before final approval.

@tsahimatsliah

Copy link
Copy Markdown
Member Author

@capJavert good catch — fixed in ceff7d8.

The card is now an anchor to the same smart link (https://r.daily.dev/get) rather than a <div> wrapping a decorative SVG, so the destination is reachable by pointer and by keyboard, not only by scanning.

Two details worth calling out:

  • The link is named by its destination, not by the visible copy. It carries aria-label="Get the daily.dev app for iOS or Android". "Scan to get the app" describes the QR, which is no use to anyone who cannot scan it, so it would have been a poor accessible name. The QR SVG stays aria-hidden.
  • I made the whole card the link rather than adding a visually-hidden one. You said hidden was acceptable, but a sighted desktop user has the same problem — they can see a QR they cannot scan from the machine they are on. This way the card is clickable for them too, and there is one destination instead of two. It also picks up a focus-visible ring, so the focus target is visible.

Test coverage in LandingAppInstall.spec.tsx: one asserts the link exists with the right href and rel, one asserts the scan copy does not become the accessible name (guards against someone later dropping the aria-label and silently regressing it).

On the real-device scan: still outstanding and I can't do it from here. I've verified the encoding round-trips to an identical matrix and that the smart link resolves correctly per platform (iPhone UA → apps.apple.com, Android → play.google.com), but a camera against the rendered code with the logo badge over it needs a human. Worth doing before approval as you say — the badge is a solid dark block over the centre, which level H should absorb, but that's theory until someone points a phone at it.

Also rebased onto latest main since the branch had fallen behind.

@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.

Reviewed by AI.

One blocking accessibility finding remains. CI is green, and I independently verified that the baked QR payload decodes to https://r.daily.dev/get.

tsahimatsliah and others added 5 commits July 30, 2026 09:54
Adds a `panel` background to the signup wall (FunnelStepType.HeroLanding):
the form in a left column with the marketing landing page's hero artwork
framed in the right one. Stacks below `laptop`, where the artwork takes the
top of the screen and dissolves into the page background.

Selected by Freyja config via the step's `background` parameter, so the
default (`cards`) is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A short desktop window (1440x700 is a common laptop size) matched the
max-height query and inherited phone-sized type in the two-column layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The QR was the only route to the app stores, and it is decorative markup
(aria-hidden) that only helps someone holding a phone. The card is now an
anchor to the same smart link, named by its destination, so keyboard and
screen-reader users have the same route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WCAG 2.5.3 Label in Name: the accessible name replaced the visible "Scan
to get the app" caption instead of containing it, so a voice-control user
could not activate the link by speaking the words on screen.

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

Copy link
Copy Markdown
Member Author

@capJavert the blocking WCAG 2.5.3 finding is fixed in f4ac07b — details in the inline thread.

Short version: the accessible name now leads with the visible caption verbatim (Scan to get the app — daily.dev for iOS or Android), the caption is a single shared constant so the rendered text and the aria-label can't drift apart, and the test that asserted the opposite is replaced by one asserting the label is contained.

All checks green, branch is up to date with main.

Two things still open, neither blocking from my side:

  1. Real-device QR scan. You've decoded the payload, which covers the encoding. What's left is whether a camera reads it through the dark logo badge in the centre — needs a human with a phone.
  2. className.onboardingSignup is dead code across the auth stack (declared in OnboardingRegistrationForm, forwarded by AuthOptionsInner, never destructured). Pre-existing, noted in Known gaps, deliberately not touched here.

Comment thread packages/shared/src/features/onboarding/components/OnboardingSignupHero.tsx Outdated
The legal row was hidden below laptop, which dropped the Terms of Service
and Privacy Policy disclosure on phones and tablets even though the form is
submittable there. The disclaimer now renders at every breakpoint; only the
general footer links stay desktop-only.

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

Copy link
Copy Markdown
Member Author

@capJavert the legal-row finding is fixed in d34e30b — details in the inline thread.

SignupDisclaimer now renders at every breakpoint; only FooterLinks stays desktop-only. Stacked spacing adjusted so it isn't cramped, desktop untouched. 320×568, 375×667 and 390×844 all still fit the viewport exactly.

Both of your findings are now closed:

  • WCAG 2.5.3 Label in Name → f4ac07b
  • Missing ToS/Privacy disclosure when stacked → d34e30b

The tests for both are structural rather than presence-based, since jsdom doesn't evaluate media queries — a hidden element is still in the DOM, so getByTestId would have passed against the broken code. I verified each new test fails when its fix is reverted.

All checks green. The only thing still outstanding is the real-device QR scan through the centre logo badge.

…er walls

The cards and desk walls render neither FooterLinks nor SignupDisclaimer
below tablet: their isMobile branch omits both. Reveal the disclosure from
tablet up so the panel matches what ships today instead of becoming the only
wall with a phone-width disclosure.

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

Copy link
Copy Markdown
Member Author

Correction on the legal-row fix — details in the inline thread.

I checked the premise against the shipping cards wall and it only half holds. Measured:

viewport SignupDisclaimer on cards
390×844 (phone) not rendered at all
700×900 (tablet) rendered and visible

OnboardingSignupHero's isMobile branch resolves to below tablet (656px) and omits both FooterLinks and SignupDisclaimer. So production has no signup disclosure at phone widths today — the tablet regression was real, the phone one was not.

My first fix overcorrected and would have made panel the only wall in the codebase with a phone-width disclosure. c752076 gates it at tablet, matching cards/desk exactly. Verified: hidden at 390, visible at 700 and 1440; footer links laptop-only throughout.

The phone gap is pre-existing and product-wide. It's a legal question rather than a layout one and fixing it only here would leave cards/desk inconsistent, so I've documented it under Known gaps rather than folding it into this PR. Say the word if you'd rather it be handled here.

All checks green.

@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.

Reviewed by AI.

Approved through AI review.

@tsahimatsliah please verify the QR code on a real device in production after merge, including that it remains scannable through the center logo badge.

The legal row supplied that spacing while it rendered at every breakpoint.
Gating it at tablet left the login link flush against the viewport edge on
phones, with nothing under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah
tsahimatsliah merged commit bda295e into main Jul 30, 2026
12 checks passed
@tsahimatsliah
tsahimatsliah deleted the claude/signup-wall-panel branch July 30, 2026 08:48
@tsahimatsliah

Copy link
Copy Markdown
Member Author

How to run this as a 50/50 experiment

Short version: there is no GrowthBook flag for this variant today, so there is no key you can set in the GrowthBook UI to turn it on. Documenting both routes so we can pick one deliberately.

Why there is no flag

The signup wall is a funnel step served by the API, not a client-gated component:

/onboarding
  └─ getServerSideProps → getFunnelBootData({ featureKey: 'onboarding' })
      └─ GET {apiUrl}/boot/funnels/onboarding
          └─ funnelState.funnel.chapters[].steps[]
              └─ { type: 'heroLanding', parameters: { background: 'panel' } }

FunnelStepper maps heroLandingFunnelHeroLanding, which reads parameters.background. Verified: FunnelHeroLanding, OnboardingSignupHero and FunnelStepper contain no useFeature / useConditionalFeature / getFeature calls at all. The only GrowthBook flag on /onboarding is swipe_onboarding, which swaps the EditTags step and is unrelated. FunnelBootData is { session, funnel } — it carries no experiment or variant key for the client to read.

So background: 'panel' can only arrive from the funnel JSON.

Option A — split in Freyja (works today, no code change)

Two funnel variants, identical apart from the heroLanding step:

arm heroLanding.parameters
control (50%) omit background, or { "background": "cards" }
panel (50%) { "background": "panel" }

Traffic allocation is configured on the funnel in Freyja. Nothing in this repo needs to change, and the default stays cards for anyone not in the experiment.

Preview a specific funnel before allocating traffic — getServerSideProps forwards both params straight to the boot call:

/onboarding?id=<funnelId>&version=<version>

Note /onboarding redirects to / on localhost, so this needs a deployed environment.

Open question for whoever owns the API: does /boot/funnels/onboarding resolve the funnel through a GrowthBook experiment server-side? That code isn't in this repo, so I can't confirm it either way. If it does, the split is already GrowthBook-driven — it just lives on the backend, and the thing being split is which funnel JSON is returned.

Option B — add a GrowthBook flag (needs a code change; not in this PR)

If we want the split owned in GrowthBook rather than Freyja, the precedent already exists in this file — swipe_onboarding gates a step override the same way:

// packages/shared/src/lib/featureManagement.ts
export const signupWallPanelFeature = new Feature('signup_wall_panel', false);

GrowthBook config for a 50/50:

field value
key signup_wall_panel
type boolean
default false
rule Experiment, 50% false (control) / 50% true (panel)

⚠️ The committed default must stay false. Per the repo's AGENTS.md, defaulting an experiment flag to true ships it to 100% of users on merge with no rollback short of a deploy. Control is the default arm.

This is maybe 15 lines: the flag, a useConditionalFeature call in packages/webapp/pages/onboarding.tsx, and forcing background: 'panel' on the heroLanding step when it resolves true.

Which one?

Option A is zero-risk and available now. Option B is worth it if we want the allocation, targeting and results to live alongside our other experiments in GrowthBook rather than in funnel config.

Happy to implement Option B on this PR if that's the direction — say the word and I'll add it behind a false default.

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