Skip to content

feat(onboarding): horizon signup wall — flag-gated full-bleed hero variant - #6491

Merged
tsahimatsliah merged 9 commits into
mainfrom
feat/signup-wall-horizon
Aug 26, 2026
Merged

feat(onboarding): horizon signup wall — flag-gated full-bleed hero variant#6491
tsahimatsliah merged 9 commits into
mainfrom
feat/signup-wall-horizon

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Aug 18, 2026

Copy link
Copy Markdown
Member

What

A fourth signup wall, horizon, behind the signup_wall_horizon flag — to run against the served one (cards) after the panel lost its test.

The marketing homepage's hero artwork is the right half, full-bleed: no frame, no overlays, nothing to read on the image, dissolving into the page at the seam. The rail carries the tagline and the auth options. Stacked, it takes the panel's shape — artwork band on top, form bottom-anchored underneath for thumb reach.

Why it should beat the panel

  • The panel framed the artwork and put a QR card on it, which added a second CTA competing with signup. The horizon has one job on screen.
  • The panel's splitSignupStyle silently rewrote "Continue with…" to "Sign up with…", which is the wrong door for every returning visitor — those buttons log existing users straight in. The horizon keeps "Continue with…".
  • Three identical solid buttons recommend nothing. Google is the only solid primary; GitHub steps down to a filled secondary (not an outline — a hairline-only button reads as disabled next to a solid one); email is a text link.
  • Continuity with the marketing homepage, which uses the same artwork.

Auth CTA hierarchy

splitSignupStyle had geometry, copy and hierarchy welded into one boolean. It becomes a signupStyle discriminant, so a wall names its treatment and the illegal combinations are unrepresentable:

signupStyle copy CTA hierarchy used by
splitCreateAccount "Sign up with…" / "Create account" all providers solid, or divider panel
singlePrimary "Continue with…" one solid primary, rest secondary, email as a text link, no divider horizon

Both imply the split-column geometry. cards and desk pass nothing and are untouched.

Provider marks are sized to the label (20px), not the button. The rail is 360px while the copy above keeps a 440px measure, so the CTA stack reads as one glanceable target. The email link is a plain <button> rather than Button — the variant's box, shadow and hover --button-background would each need overriding to look like a link — with a min-h-12 row, so the tap target clears 44px while the label stays a 20px link.

Experiment readiness

Follows the flag rules in AGENTS.md: declared in featureManagement.ts, defaults to the control (false), evaluated through useConditionalFeature with shouldEvaluate.

  • Enrollment is the whole onboarding funnel (isAuthReady && isOnboardingFunnel) — the same shape as swipe_onboarding and the panel's own test, with no cohort carve-outs. Evaluating is what allocates: getFeatureValue fires GrowthBook's trackingCallback, which POSTs experiment_id/variation_id. Users who evaluate and are then auto-transitioned past the step dilute both arms symmetrically — a sliver of power, not a bias. The paid funnel never enrolls. Pinned by FunnelHeroLanding.spec.tsx.
  • No control-arm contamination. The wall holds render until the flag resolves, so treatment users never paint a frame of the served control — here a full-screen background swap plus a wasted hero download. Bounded at 200ms, armed only for visits being enrolled: isAuthReady and GrowthBook's ready come out of the same boot payload one commit apart, so the deadline is a safety net, not a budget. Kept short because ready never flips when boot returns no experiment features, and a longer wait would blank the funnel's entry screen for both arms.
  • Two ways to run it: flip the flag in GrowthBook (use an Experiment rule — a Force rule fires no exposure), or serve background: 'horizon' from Freyja exactly as the panel's test was run. The Google-first order keys off the resolved background, not the flag, so the Freyja path is complete on its own.
  • Funnel events (funnel step view, transition funnel, complete funnel, leave funnel) are step-level and background-agnostic — unchanged. Every provider and the email link keep data-funnel-track="signup provider", so click funnel element fires with identical target ids across arms.

Scope

Trimmed to what the experiment needs:

  • No value line / subline — the wall is headline-only, so the funnel param, the default constant and its plumbing are all gone.
  • The stacked band reuses the panel's .onb-art-half, overriding only the short-viewport height (this column has no divider and no bordered email button, so the panel's 32dvh would leave a dead gap).
  • SignupWallComparison.stories.tsx already exists on main; this adds one frame entry to it rather than new comparison stories.
  • The artwork markup lives in one HorizonArt component with two named crops.

Known follow-up

Responsive image variants — worth doing before this ramps far. The hero is a single signed Cloudinary URL; the signature covers the transformation, so w_768 404s rather than resizing (verified against the live URL) and phones pull the full asset. Sized derivatives must be exported and signed upstream before a srcSet can exist. Until then the stacked band requests at fetchpriority="low" so it doesn't race the auth options on a cold mobile connection; the desktop column, which is half the screen, keeps high.

Test plan

  • Storybook — Components/Onboarding/Steps/FunnelHeroLanding → Horizon, and the existing Signup wall comparison page, which now includes it
  • Verified at 1440×900, 390×844 and 375×667; email link measured at 48px with hover keeping background-color: rgba(0,0,0,0) and box-shadow: none while moving the label to full strength
  • Regression: Cards and Panel checked for button copy, variants, divider, borders and icon sizes after the signupStyle refactor
  • lint_shared, test_shared (209 passing across onboarding + auth, including 10 enrollment/hold specs) and prettier run locally before pushing
  • Flag off (default): the new code is reachable only via background: 'horizon'

🤖 Generated with Claude Code

Preview domain

https://feat-signup-wall-horizon.preview.app.daily.dev

@vercel

vercel Bot commented Aug 18, 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 Aug 26, 2026 10:39am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
storybook Ignored Ignored Aug 26, 2026 10:39am

Request Review

@tsahimatsliah tsahimatsliah left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Summary

A new flag-gated horizon signup-wall variant reusing the panel's split-column machinery, plus a three-way split of the old splitSignupStyle boolean so a wall can take the geometry without the copy or the CTA hierarchy. CI is green, the diff is scoped to the variant, control walls (cards, panel, desk) look byte-identical, and the sanitizer/HeroBackgroundLayer/precedence issues the description lists as self-review fixes are genuinely fixed in the branch.

Two things I'd want resolved before this runs as an experiment, plus a handful of non-blocking notes. Details are inline.

Blocking

  • FunnelHeroLanding.tsx:105 — enrollment fires for onboarding-funnel visits that never render a wall (already-authenticated users and completed onboarding), diluting the treatment/control split on a signup-completion metric.

Question (potentially blocking)

  • FunnelHeroLanding.tsx:124 — the bounded flag hold blanks the funnel's entry screen for up to 1s for both arms whenever boot arrives without experiment features. swipe_onboarding, cited as the precedent, does not hold render.

Non-blocking

  • Email CTA tap target and the !important stack (OnboardingRegistrationForm.tsx:208)
  • Three interdependent style booleans on a shared auth component (common.tsx:139)
  • Duplicated inline artwork markup vs. the existing LandingHeroCover component (OnboardingSignupHero.tsx:229)
  • Unsized hero asset at fetchpriority="high" on the funnel entry screen (image.ts:161)
  • No test coverage for the flag override / hold logic itself

Verification

  • Root + package AGENTS.md read; scope matches the stated task
  • Flag/enrollment path traced through useConditionalFeatureFeaturesReadyContexttrackingCallback → allocation POST
  • Control walls and heroStyles compact-phone rules checked for inherited-compensation loss (horizon correctly reuses onb-hero-main / onb-split-cta / onb-split-login)
  • CI inspected: all checks passing; branch is behind main and needs an update before merge
  • Not verified by me: preview at 390×844 / 375×667, and the artwork's real LCP on a cold cache

Verdict

Comment — no code-correctness blocker, but the enrollment scope should be fixed before this is turned on as an experiment.

Reviewed by AI.

Comment thread packages/shared/src/features/onboarding/steps/FunnelHeroLanding.tsx Outdated
Comment thread packages/shared/src/features/onboarding/steps/FunnelHeroLanding.tsx
Comment thread packages/shared/src/components/auth/OnboardingRegistrationForm.tsx Outdated
Comment thread packages/shared/src/components/auth/common.tsx Outdated
Comment thread packages/shared/src/features/onboarding/components/OnboardingSignupHero.tsx Outdated
Comment thread packages/shared/src/lib/image.ts
@tsahimatsliah

Copy link
Copy Markdown
Member Author

Review follow-ups — all six addressed

One commit: 55cee74. All six comments are resolved in code rather than deferred, including the four marked non-blocking — the API-shape one in particular was cheap now and would only get more expensive with a third wall.

# Comment Status What changed
1 Blocking — enrollment fires for visits that never see a wall Fixed shouldEvaluate mirrors the render predicate
2 Question — the 1s hold blanks the entry screen for both arms Answered + fixed 200 ms, armed only when enrolling
3 Email CTA tap target and the !important stack Fixed Plain button, 48px row, zero overrides
4 Three interdependent booleans on a shared auth surface Fixed One signupStyle discriminant
5 Duplicated artwork markup Fixed New HorizonArt component
6 Unsized hero at fetchpriority="high" Mitigated + verified Band demoted to low; both claims checked

The blocking one

shouldEvaluate was isAuthReady && isOnboarding, but the step returns null a few lines later for users who are already authenticated and confirmed, or re-entering with onboarding complete. Since evaluating is what fires trackingCallback, those visits were allocating without seeing a wall — the denominator problem you described. It now mirrors the render:

const isSkippingWall =
  (isLoggedIn && user?.infoConfirmed) ||
  isOnboardingComplete ||
  (isLoggedIn && !isOnboardingActionsReady);

const shouldEvaluateWallFlag = isAuthReady && isOnboarding && !isSkippingWall;

The third clause is the same argument applied one step further: a signed-in user's isOnboardingComplete is false until their actions land, so without it we would enroll during the fetch and then transition them past the step. Anonymous visitors have nothing to fetch and are never held.

On the hold

You were right to push on it. isAuthReady and GrowthBook's ready come out of the same boot payload one commit apart — BootProvider sets initialLoad and writes exp together, and GrowthBookProvider's effect calls setReady on the next commit — so 1000 ms was never a budget for anything. It is 200 ms now, armed only when the visit is being enrolled (which also fixes the paid-funnel timer nit).

I kept a hold rather than following swipe_onboarding exactly, because what flashes here is a full-screen background swap plus a wasted hero download rather than a widget. But that is a reason to wait, not a reason to wait long, and the pathological case you found now costs a blink instead of a blank second for the control arm.

New coverage

FunnelHeroLanding.spec.tsx — 10 specs, all green — asserts shouldEvaluate for anonymous wall viewer (true), authenticated + confirmed (false, no wall), onboarding complete (false), signed-in with actions loading (false), auth not ready (false), paid funnel (false); the hold releasing to the served background after the deadline; a non-enrolled visit never being held; and the background → signupStyle mapping in both directions.

Suite: 211 passing across onboarding + auth. lint_shared and prettier clean.

Verified, not assumed

  • w_768 on the signed hero URL returns 404; the base URL returns 200. Resizing genuinely is blocked upstream.
  • The URL is byte-identical to what main-site's HomepageExperiment.astro preloads, and Chrome partitions the HTTP cache by top-level site (which daily.dev and app.daily.dev share) — so the warm-cache hit is real, but only for traffic arriving from a homepage variant that is itself an experiment. That caveat is now in image.ts; the PR no longer leans on it.
  • Email link measured at 48px with a transparent background and no shadow at rest and on hover.
  • Cards and Panel re-checked in Storybook after the discriminant refactor: same copy, variants, divider, borders, icon sizes.

One thing I did not touch

LandingHeroCover.tsx passes fetchPriority in camelCase, which React 18 warns about on every render (React does not recognize the fetchPriority prop…). It is pre-existing on main and on the panel path, not this branch — flagging it rather than widening the diff into the previously-tested arm.

Ready for another look.

A flag-gated fourth signup wall, to run against the served one (`cards`)
after the panel lost its test. The marketing homepage's hero artwork is
the right half full-bleed — no frame, no overlays, nothing to read on
the image — dissolving into the page at the seam, with the tagline and
the auth options on the rail. Stacked, it takes the panel's shape: an
artwork band on top, the form bottom-anchored underneath for thumb
reach. The band keeps more height than the panel's on short viewports,
because this column has no divider and no bordered email button.

The CTA stack is the other half of the bet. One solid primary (Google),
GitHub filled-secondary rather than an outline, and email demoted to a
text link — a wall that recommends a default instead of offering three
identical doors. `splitSignupStyle` had geometry, copy and hierarchy
welded into one boolean; it becomes a `signupStyle` discriminant so the
horizon can take the geometry while keeping "Continue with…", which
logs returning users straight in rather than building a wrong door.

Enrollment mirrors the step's render predicate, not just "is this the
onboarding funnel": evaluating the flag is what fires GrowthBook's
trackingCallback, so a visit that never paints a wall — already
authenticated and confirmed, or onboarding already complete — must not
allocate, or it sits in the denominator of both arms unable to convert.
The wall holds render until the flag resolves so treatment users never
paint a frame of the control, bounded at 200ms because `ready` never
flips when boot returns no experiment features.

Flag defaults to `false`. Cards, desk and panel are untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah
tsahimatsliah force-pushed the feat/signup-wall-horizon branch from 55cee74 to a793452 Compare August 23, 2026 12:18
@tsahimatsliah

Copy link
Copy Markdown
Member Author

Rebased onto main and trimmed

Squashed the 13 commits (two of them merge commits) into one on top of current main, and cut everything that wasn't the experiment. Diff is now 15 files, +691/−55 — and 289 of those additions are tests.

Removed

  • The value line. The wall is headline-only now. That took out the subline funnel param, the HORIZON_DEFAULT_SUBLINE constant, its plumbing through the step and the hero, the story arg and its spec.
  • The bespoke comparison stories. SignupWallComparison.stories.tsx already exists on main (added by the panel PR) — I'd bolted a two-up harness onto it for the design review. Reverted to main's version plus one frame entry so the existing tool covers the new wall: 5 lines instead of 48.
  • A duplicate CSS rule. The band reused the panel's .onb-art-half instead of carrying a near-copy, keeping only a short-viewport override.
  • An unused union member. SignupStyle is the two values that exist, not three.
  • ~90 lines of commentary across the flag, the image constant, HorizonArt and the step — kept the reasoning that isn't recoverable from the code, cut the essays.

Note on the band height

Removing the value line shortened the column by ~100px, so the panel's compact-phone step-down (32dvh) opened a visible dead gap between the fade and the logo on a 375×667 screen. The band now overrides to 40dvh there only — measured gap went from ~150px to 86px, which reads as deliberate spacing for a bottom-anchored layout rather than a hole. Tall phones and desktop are unchanged.

Verified after the trim

  • 209 tests passing across onboarding + auth; lint_shared and prettier clean on every changed file
  • Horizon re-checked at 1440×900, 390×844 and 375×667 — no subtitle, artwork and CTA stack unchanged
  • Cards and Panel re-checked: same copy, variants, divider, borders and icon sizes

One thing I deliberately kept

The signupStyle refactor still touches three shared auth files. It isn't optional — the horizon's CTA hierarchy (one solid primary, text-link email, no divider) needs a way in, and on main that boolean also silently rewrites "Continue with…" to "Sign up with…", which is the wrong door for returning users. It's a net reduction in props, and Cards/Panel behaviour is unchanged. Happy to revisit if the team would rather see it split out.

@tsahimatsliah tsahimatsliah left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Re-review (branch rewritten after the 20 Aug pass)

Every item from the last round is genuinely addressed, and a couple went further than I asked:

  • EnrollmentshouldEvaluate now mirrors the render predicate via isSkippingWall, with six specs pinning each path. The third clause (signed-in user whose actions haven't landed) is the one I didn't ask for and it is correct. ✅
  • Flag hold — 1000ms → 200ms, armed only when the visit is being enrolled, with the boot-ordering reasoning recorded in the constant. The paid-funnel timer nit fell out with it. ✅
  • Email CTA — plain <button>, nine !importants gone, min-h-12 for a 48px target under a 20px label. ✅
  • signupStyle discriminant — done rather than deferred; git grep splitSignupStyle on the branch is clean across all four files, and two specs assert the panel/cards mapping. ✅
  • HorizonArt — one <img>, two named crops. Agreed on keeping it separate from LandingHeroCover rather than adding a variant that opts out of the component's reason for existing. ✅
  • Image — resize 404 and the cache-partition claim both verified rather than asserted, and the description now treats signed derivatives as the ramp prerequisite. ✅

Three new things, one of which undercuts the fetchpriority mitigation added in response to the last round. None are large.

Findings

  1. Both HorizonArt instances are always mounted (CSS-hidden only), so the high/low split doesn't do what the comment says.
  2. A signed-in, unconfirmed user can still paint the control wall and then flip to horizon — the exact flash the hold exists to prevent, in the one cohort the hold deliberately doesn't cover.
  3. Small polish: a reintroduced !important trio, a nested ternary behind an eslint-disable, and an aria-label that disagrees with its visible label.

Notes, not findings

  • funnel step view is logged by useFunnelTracking off step?.id, so it fires during the hold and for visits that never paint a wall. Pre-existing and arguably correct, but it means step-view is no longer a proxy for wall-view in this experiment's analysis.
  • The treatment moves four things at once against cards: artwork/layout, single-primary hierarchy, email demoted to a link, and Google-first ordering (forced by background === 'horizon', overriding the developer-first default). Deliberate as a wall-level test, but a win won't attribute to any one of them — worth naming which you'd keep if it wins.
  • showOrbs / imageMode / imageMobile are silently inert on horizon, so a Freyja step serving them plus the flag on gets none of them.

Verification

  • CI green; git grep splitSignupStyle clean on the branch
  • Re-read the enrollment gate, the hold, and both useConditionalFeature paths
  • Confirmed step-view logging is funnel-level, so the hold doesn't drop the event
  • Not verified by me: the measured 48px email row and the 375/390 layouts (Storybook is the evidence)

Reviewed by AI.

Comment thread packages/shared/src/features/onboarding/components/signupHero/HorizonArt.tsx Outdated
Comment thread packages/shared/src/features/onboarding/steps/FunnelHeroLanding.tsx
Comment thread packages/shared/src/components/auth/OnboardingRegistrationForm.tsx Outdated
Comment thread packages/shared/src/components/auth/OnboardingRegistrationForm.tsx Outdated
Comment thread packages/shared/src/components/auth/OnboardingRegistrationForm.tsx Outdated
Review follow-ups, plus a comment pass.

Both artwork elements are in the DOM at every width — the wrappers use
Tailwind visibility, and `display: none` does not stop an <img> loading
— so the band's `low` and the column's `high` were two hints for one
deduped request, resolved by DOM order rather than by viewport. The
split claimed a mobile protection that was never in effect. One hint
now, and the comment says what actually ships.

`isWallPending` required `shouldEvaluateWallFlag`, which is false while
a signed-in user's onboarding actions are still loading — so that visit
painted the served wall, then swapped to horizon in the same commit
once the actions landed. The control-arm flash the hold exists to
prevent, reached through the actions fetch instead of the flag fetch.
Enrollment resolves through both, so both are now held, with a spec
covering the cohort.

The stepped-down provider takes ButtonVariant.Float, which is tertiary
plus surface-float plus a hairline — what two of the three overrides
were recreating by hand. One override left, for the label. The nested
ternary becomes getProviderVariant, matching the other getters in the
file and dropping its eslint-disable. The email link loses an
aria-label that read "Signup using email" over a visible "Continue with
email", so its accessible name no longer contradicts what is on screen.

Comments cut back to what the code cannot say itself: the flag default
rule, why evaluating enrolls, why the hold is bounded, that `w_768`
404s, and the two crops' geometry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah
tsahimatsliah force-pushed the feat/signup-wall-horizon branch from 5ff51c1 to 697374d Compare August 23, 2026 13:04
@tsahimatsliah

Copy link
Copy Markdown
Member Author

All five addressed, plus a comment pass — and one bug I found reviewing my own fix

Two commits on the branch now; the second is the review round so the delta stays readable.

Finding Status
fetchpriority split doesn't bind Fixed — one hint, comment corrected
Hold gap for signed-in mid-actions-fetch Fixed — both paths held, spec'd
Three !importants on Secondary FixedButtonVariant.Float already is that rung
Nested ternary + eslint-disable FixedgetProviderVariant(index)
aria-label contradicts visible label Fixed — dropped on the new element

The one that mattered most

You were right that the priority split was fiction. Both wrappers are Tailwind visibility, display: none doesn't stop an <img> loading, so it was one deduped request with two hints resolved by DOM order.

I took your second option rather than the viewport hook: useViewSize reads matchMedia in its useState initialiser so it's right on the first client render, but this route is server-rendered with ssrValue: false, and swapping whole elements on hydration would mismatch the desktop LCP element. A class mismatch this component already tolerates; an <img> swap is a worse trade than the problem it solves. One high hint on both, and the comment now states the real constraint.

Float was already the rung you described

tertiaryFloat is tertiary + background: var(--theme-surface-float) + a border-subtlest-primary hairline — exactly what two of the three overrides were hand-rolling. One override left (!text-text-primary), because tertiary's label is text-secondary and the brand mark follows it through currentColor. Verified in the DOM: background srgb(0.659 0.702 0.808 / 0.08), hairline at / 0.15, label and icon both rgb(255,255,255). Visually identical to what was signed off, no new variant.

A bug in my own fix

Applying your isWallPending shape verbatim introduced an unbounded hold: isResolvingEnrollment isn't gated by any deadline, so a signed-in user whose actions request never resolves would sit on a permanently blank entry screen — the exact failure the flag deadline exists to prevent, reintroduced through the other branch.

Both holds are bounded now, with deadlines matched to what's actually being waited on: 200ms for the flag (rides the boot payload, lands a commit after auth) and 1000ms for onboarding actions (a real round trip, so a short bound would expire mid-fetch and put the flash back). Spec added for the never-resolves case.

Comments

Cut from ~130 lines to ~50, keeping only what the code can't say: the flag default rule, why evaluating enrolls, why each hold is bounded, that w_768 404s, why both artwork elements share one hint, and the two crops' geometry. Everything restating the code is gone.

211 tests passing (12 in FunnelHeroLanding.spec), lint and prettier clean, Cards and Panel re-verified unchanged in Storybook.

tsahimatsliah added a commit that referenced this pull request Aug 23, 2026
Ports the signupStyle refactor from #6491 verbatim — the splitSignupStyle
boolean becomes a discriminant ('splitCreateAccount' keeps the panel's
treatment, 'singlePrimary' is the horizon's) — and opts the strip banner
into singlePrimary: Google is the one solid primary, GitHub steps down to
a fill, email becomes a text link and the divider goes. preferGithub is
off so the solid button leads the stack.

Byte-identical to the #6491 versions of the three auth files, so whichever
PR lands second merges clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tsahimatsliah added a commit that referenced this pull request Aug 23, 2026
Ports the signupStyle refactor from #6491 verbatim — the splitSignupStyle
boolean becomes a discriminant ('splitCreateAccount' keeps the panel's
treatment, 'singlePrimary' is the horizon's) — and opts the strip banner
into singlePrimary: Google is the one solid primary, GitHub steps down to
a fill, email becomes a text link and the divider goes. preferGithub is
off so the solid button leads the stack.

Byte-identical to the #6491 versions of the three auth files, so whichever
PR lands second merges clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment on lines +91 to +133
// A signed-in user's completion is only known once their actions land;
// until then we can't tell a wall viewer from someone about to be
// transitioned past this step. Anonymous visitors have nothing to fetch.
const isResolvingEnrollment = isLoggedIn && !isOnboardingActionsReady;
// Enrollment must not fire for a visit that never paints a wall, or it
// sits in the denominator of both arms unable to convert.
const isSkippingWall =
(isLoggedIn && user?.infoConfirmed) ||
isOnboardingComplete ||
isResolvingEnrollment;
// Evaluating is what enrolls: `getFeatureValue` fires GrowthBook's
// trackingCallback, which POSTs the allocation.
const shouldEvaluateWallFlag =
isAuthReady && isOnboarding && !isSkippingWall;
const { value: isHorizonWallEnabled, isLoading: isHorizonFlagLoading } =
useConditionalFeature({
feature: featureSignupWallHorizon,
shouldEvaluate: shouldEvaluateWallFlag,
});
const isHoldingForEnrollment =
isResolvingEnrollment || (shouldEvaluateWallFlag && isHorizonFlagLoading);
const holdTimeoutMs = isLoggedIn
? ACTIONS_RESOLVE_TIMEOUT_MS
: FLAG_RESOLVE_TIMEOUT_MS;
const [hasWaitedForEnrollment, setHasWaitedForEnrollment] = useState(false);
useEffect(() => {
if (!isHoldingForEnrollment) {
return undefined;
}
const timeout = setTimeout(
() => setHasWaitedForEnrollment(true),
holdTimeoutMs,
);
return () => clearTimeout(timeout);
}, [isHoldingForEnrollment, holdTimeoutMs]);
// Painting the served wall and swapping later would show the control arm
// to treatment users and waste a hero download. Enrollment resolves
// through the actions fetch as well as the flag, so both are held.
const isWallPending = isHoldingForEnrollment && !hasWaitedForEnrollment;
const background = isHorizonWallEnabled ? 'horizon' : backgroundParam;
const isHorizonWall = background === 'horizon';
const oauthOrder =
oauthOrderParam ?? (isHorizonWall ? 'googleFirst' : undefined);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

what's this? looks like a huge slop. also why do you touch the order of the oauth?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fair on both counts.

The slop: agreed — that block accumulated derived state across three review rounds and ended up over-engineered (and hiding two bugs). Just pushed 969d4f8 which deletes most of it: enrollment is now anonymous onboarding visitors only — one predicate, one 200ms safety-net timeout, net −48 lines. Logged-in users keep the served wall and stay out of the experiment entirely, which is also better experiment hygiene: they get routed to the social-registration display anyway, not the CTA stack this wall redesigns, and the metric is anonymous→signup.

The OAuth order: deliberate, not incidental. The horizon design is one solid primary CTA instead of two identical white buttons, and Google was picked as that primary (broadest one-click path). Order and primacy are the same decision here — the first provider gets the solid treatment. Freyja-served oauthOrder still wins; the code only supplies the default on the flag path so the flag renders the approved design.

Honest caveat: it does bundle two changes into one test (layout + provider order), so a win/loss won't attribute between them. If you'd rather isolate the layout, I can drop the default and keep GitHub-first as the solid primary — say the word and it's a two-line change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

anonymous onboarding visitors only — one predicate, one 200ms safety-net timeout, net −48 lines. Logged-in users keep the served wall and stay out of the experiment entirely, which is also better experiment hygiene: they get routed to the social-registration display anyway, not the CTA stack this wall redesigns, and the metric is anonymous→signup.

what is this BS? this is just a weird hack. why should we keep them out of the experiment?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

You're right — carve-out removed in fa2f570. Enrollment is now just isAuthReady && isOnboardingFunnel, same as swipe_onboarding: everyone who reaches the step evaluates, logged-in included, and the flag resolves off the boot payload for all of them. No cohort logic left — one predicate and a bounded 200ms hold so treatment users never flash the control.

rebelchris pushed a commit that referenced this pull request Aug 24, 2026
Ports the signupStyle refactor from #6491 verbatim — the splitSignupStyle
boolean becomes a discriminant ('splitCreateAccount' keeps the panel's
treatment, 'singlePrimary' is the horizon's) — and opts the strip banner
into singlePrimary: Google is the one solid primary, GitHub steps down to
a fill, email becomes a text link and the divider goes. preferGithub is
off so the solid button leads the stack.

Byte-identical to the #6491 versions of the three auth files, so whichever
PR lands second merges clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The enrollment/hold block had grown to seven pieces of derived state and
two timeouts across three review rounds, and a fresh review found two
bugs inside it: the actions hold was not gated on the onboarding funnel,
so the paid funnel's entry screen blanked for logged-in users, and the
hold deadline keyed off who the user was rather than which fetch was
being waited on.

The machinery existed to handle logged-in users' enrollment edge cases —
but logged-in users don't belong in this experiment. The metric is
anonymous-visitor signup conversion, and a signed-in unconfirmed user is
routed to the social-registration display rather than the CTA stack the
wall redesigns. Enrollment is now anonymous onboarding visitors only,
who always render a wall, so exposure still fires exactly for wall
viewers; logged-in visits keep the served wall, unheld and unenrolled.
Both bugs die with the code they lived in: one predicate, one 200ms
safety-net timeout.

Also guards the email link's hover while disabled, so it stops
brightening as if clickable during social auth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah
tsahimatsliah force-pushed the feat/signup-wall-horizon branch from 969d4f8 to 74af3c4 Compare August 24, 2026 09:58
tsahimatsliah added a commit that referenced this pull request Aug 24, 2026
Ports the signupStyle refactor from #6491 verbatim — the splitSignupStyle
boolean becomes a discriminant ('splitCreateAccount' keeps the panel's
treatment, 'singlePrimary' is the horizon's) — and opts the strip banner
into singlePrimary: Google is the one solid primary, GitHub steps down to
a fill, email becomes a text link and the divider goes. preferGithub is
off so the solid button leads the stack.

Byte-identical to the #6491 versions of the three auth files, so whichever
PR lands second merges clean.

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

Review pushback: excluding logged-in users from the experiment was a
carve-out nobody asked for. Enrollment is now simply "onboarding funnel,
auth ready" — the same shape as swipe_onboarding and the panel's own
test — so everyone who reaches the step evaluates, logged-in included,
and the flag resolves off the boot payload at the same moment for all
of them. One predicate, one bounded 200ms hold, nothing special-cased.

The cost is the small symmetric dilution from users who evaluate and
are then auto-transitioned past the step; it lands in both arms alike,
so it costs a sliver of power, not validity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rebelchris pushed a commit that referenced this pull request Aug 25, 2026
Ports the signupStyle refactor from #6491 verbatim — the splitSignupStyle
boolean becomes a discriminant ('splitCreateAccount' keeps the panel's
treatment, 'singlePrimary' is the horizon's) — and opts the strip banner
into singlePrimary: Google is the one solid primary, GitHub steps down to
a fill, email becomes a text link and the divider goes. preferGithub is
off so the solid button leads the stack.

Byte-identical to the #6491 versions of the three auth files, so whichever
PR lands second merges clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tsahimatsliah added a commit that referenced this pull request Aug 25, 2026
Ports the signupStyle refactor from #6491 verbatim — the splitSignupStyle
boolean becomes a discriminant ('splitCreateAccount' keeps the panel's
treatment, 'singlePrimary' is the horizon's) — and opts the strip banner
into singlePrimary: Google is the one solid primary, GitHub steps down to
a fill, email becomes a text link and the divider goes. preferGithub is
off so the solid button leads the stack.

Byte-identical to the #6491 versions of the three auth files, so whichever
PR lands second merges clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rebelchris pushed a commit that referenced this pull request Aug 25, 2026
Ports the signupStyle refactor from #6491 verbatim — the splitSignupStyle
boolean becomes a discriminant ('splitCreateAccount' keeps the panel's
treatment, 'singlePrimary' is the horizon's) — and opts the strip banner
into singlePrimary: Google is the one solid primary, GitHub steps down to
a fill, email becomes a text link and the divider goes. preferGithub is
off so the solid button leads the stack.

Byte-identical to the #6491 versions of the three auth files, so whichever
PR lands second merges clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rebelchris pushed a commit that referenced this pull request Aug 25, 2026
Ports the signupStyle refactor from #6491 verbatim — the splitSignupStyle
boolean becomes a discriminant ('splitCreateAccount' keeps the panel's
treatment, 'singlePrimary' is the horizon's) — and opts the strip banner
into singlePrimary: Google is the one solid primary, GitHub steps down to
a fill, email becomes a text link and the divider goes. preferGithub is
off so the solid button leads the stack.

Byte-identical to the #6491 versions of the three auth files, so whichever
PR lands second merges clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah
tsahimatsliah merged commit 9479e17 into main Aug 26, 2026
12 checks passed
@tsahimatsliah
tsahimatsliah deleted the feat/signup-wall-horizon branch August 26, 2026 10:45
rebelchris added a commit that referenced this pull request Aug 26, 2026
Slimmed after the horizon signup wall (#6491) merged: the SignupStyle
union, the single-primary CTA hierarchy and the AuthOptionsInner
threading this branch carried all landed with it, so what remains is
the banner-only work — the strip drops the media above the headline
(geo flag emoji, social network icon, referrer avatar) across all
personalized variants, evens its vertical padding, adopts the merged
single-primary style, and geoToEmoji leaves with its last consumer.

Co-Authored-By: Tsahi Matsliah <tsmatliah@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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