From 7281072ca781fb7e5b5ab3d2e4ecd027559aa9ed Mon Sep 17 00:00:00 2001 From: Tsahi Matsliah Date: Sun, 26 Jul 2026 12:25:49 +0300 Subject: [PATCH 1/8] feat(onboarding): panel signup wall variant 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 --- .../auth/OnboardingRegistrationForm.tsx | 54 ++++---- .../components/OnboardingSignupHero.spec.tsx | 13 ++ .../components/OnboardingSignupHero.tsx | 104 +++++++++++++++ .../signupHero/HeroBackgroundLayer.tsx | 6 + .../signupHero/LandingAppInstall.tsx | 66 ++++++++++ .../signupHero/LandingHeroCover.tsx | 109 ++++++++++++++++ .../components/signupHero/heroStyles.ts | 74 +++++++++++ .../onboarding/steps/FunnelHeroLanding.tsx | 24 +++- .../src/features/onboarding/types/funnel.ts | 8 +- packages/shared/src/lib/image.ts | 6 + .../onboarding/FunnelHeroLanding.stories.tsx | 84 ++++++++++++ .../SignupWallComparison.stories.tsx | 122 ++++++++++++++++++ 12 files changed, 643 insertions(+), 27 deletions(-) create mode 100644 packages/shared/src/features/onboarding/components/signupHero/LandingAppInstall.tsx create mode 100644 packages/shared/src/features/onboarding/components/signupHero/LandingHeroCover.tsx create mode 100644 packages/storybook/stories/components/onboarding/FunnelHeroLanding.stories.tsx create mode 100644 packages/storybook/stories/components/onboarding/SignupWallComparison.stories.tsx diff --git a/packages/shared/src/components/auth/OnboardingRegistrationForm.tsx b/packages/shared/src/components/auth/OnboardingRegistrationForm.tsx index 657564e5365..7abc740ac87 100644 --- a/packages/shared/src/components/auth/OnboardingRegistrationForm.tsx +++ b/packages/shared/src/components/auth/OnboardingRegistrationForm.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React, { useEffect } from 'react'; +import React, { cloneElement, useEffect } from 'react'; import classNames from 'classnames'; import type { AuthFormProps } from './common'; import { providerMap } from './common'; @@ -10,6 +10,7 @@ import { AuthEventNames, AuthTriggers } from '../../lib/auth'; import type { ButtonProps } from '../buttons/Button'; import { Button, ButtonSize, ButtonVariant } from '../buttons/Button'; import { isIOSNative } from '../../lib/func'; +import { IconSize } from '../Icon'; import { MemberAlready } from '../onboarding/MemberAlready'; import SignupDisclaimer from './SignupDisclaimer'; @@ -146,14 +147,22 @@ export const OnboardingRegistrationForm = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // text-primary, not white: this button sits on the page background, which is + // light in light mode — white label on it is invisible const tertiarySignupButtonClass = - '!w-full !border !border-border-subtlest-tertiary !text-white'; + '!w-full !border !border-border-subtlest-tertiary !text-text-primary'; const getEmailButtonClass = (): string => { if (compact) { return 'mb-4'; } - if (isOnboardingTrigger && !splitSignupStyle) { + // This margin, not the login link's own, is most of the gap between the CTA + // and "Already have an account". onb-split-cta lets the signup hero close + // it further on compact phones. + if (splitSignupStyle) { + return 'onb-split-cta mb-4'; + } + if (isOnboardingTrigger) { return 'mb-3'; } return 'mb-8'; @@ -189,13 +198,22 @@ export const OnboardingRegistrationForm = ({ ); const getMemberAlreadyContainerClass = (): string => { + // Stacked, the split layouts have no footer/disclaimer strip under them, so + // this centres like the cards/desk walls. The form is bottom-anchored + // there, so the tight gap is what pushes the buttons down the screen. + // Once the columns appear it follows the left-aligned column edge again. + // onb-split-login is a styling hook for the signup hero: it tightens this + // row on compact phones. Inert anywhere the hero's CSS is not present. + if (splitSignupStyle) { + return 'onb-split-login mx-auto mt-4 text-center text-text-secondary typo-callout laptop:mx-0 laptop:mt-5 laptop:text-left'; + } if (isOnboardingTrigger) { return 'mx-auto mt-5 text-center text-text-secondary typo-callout'; } return 'mx-auto mt-6 text-center text-text-secondary typo-callout'; }; - const memberAlready = !hideLoginLink && !splitSignupStyle && ( + const memberAlready = !hideLoginLink && ( onExistingEmail?.('')} className={{ @@ -205,23 +223,6 @@ export const OnboardingRegistrationForm = ({ /> ); - const splitSignInSection = splitSignupStyle && !hideLoginLink && ( -
-

- Already have an account? -

- -
- ); const disclaimer = ( ); @@ -240,7 +241,15 @@ export const OnboardingRegistrationForm = ({ className="w-full" data-funnel-track={FunnelTargetId.SignupProvider} disabled={!isReady || isSocialAuthLoading} - icon={provider.icon} + icon={ + // A Large button gives its icon IconSize.Large (32px); the + // split layouts want the brand marks a notch smaller so they + // sit closer to the label's weight. Next step down the scale + // rather than an arbitrary size. + splitSignupStyle + ? cloneElement(provider.icon, { size: IconSize.Medium }) + : provider.icon + } loading={!isReady || isSocialAuthLoading} onClick={() => onProviderClick?.(provider.value, false)} size={onboardingSignupButton?.size ?? ButtonSize.Large} @@ -268,7 +277,6 @@ export const OnboardingRegistrationForm = ({ )} > {emailButton} - {splitSignInSection} {memberAlready} ) : ( diff --git a/packages/shared/src/features/onboarding/components/OnboardingSignupHero.spec.tsx b/packages/shared/src/features/onboarding/components/OnboardingSignupHero.spec.tsx index fab49866764..6ef32db1b22 100644 --- a/packages/shared/src/features/onboarding/components/OnboardingSignupHero.spec.tsx +++ b/packages/shared/src/features/onboarding/components/OnboardingSignupHero.spec.tsx @@ -123,6 +123,19 @@ describe('OnboardingSignupHero', () => { expect(screen.queryByTestId('hero-halo')).not.toBeInTheDocument(); }); + it('renders the hero cover and the full element set for the panel background', () => { + renderHero({ background: 'panel', headline: 'Hello devs' }); + expect(screen.getAllByTestId('landing-hero-cover').length).toBeGreaterThan( + 0, + ); + expect(screen.queryByTestId('bg-layer')).not.toBeInTheDocument(); + expect(screen.getByTestId('logo')).toBeInTheDocument(); + expect(screen.getByText('Hello devs')).toBeInTheDocument(); + expect(screen.getByTestId('auth-form')).toBeInTheDocument(); + expect(screen.getByTestId('footer')).toBeInTheDocument(); + expect(screen.getByTestId('disclaimer')).toBeInTheDocument(); + }); + it('renders the form and headline', () => { renderHero({ headline: 'Hello devs' }); expect(screen.getByTestId('auth-form')).toBeInTheDocument(); diff --git a/packages/shared/src/features/onboarding/components/OnboardingSignupHero.tsx b/packages/shared/src/features/onboarding/components/OnboardingSignupHero.tsx index aada28cc335..4852c844f33 100644 --- a/packages/shared/src/features/onboarding/components/OnboardingSignupHero.tsx +++ b/packages/shared/src/features/onboarding/components/OnboardingSignupHero.tsx @@ -23,6 +23,8 @@ import type { import { HERO_STYLES } from './signupHero/heroStyles'; import { HeroBackgroundLayer } from './signupHero/HeroBackgroundLayer'; import { AuroraOrbs } from './signupHero/HeroDecorations'; +import { LandingHeroCover } from './signupHero/LandingHeroCover'; +import { LandingAppInstall } from './signupHero/LandingAppInstall'; import { cloudinaryOnboardingLoginBackground } from '../../../lib/image'; import { sanitizeMessage } from '../lib/utils'; @@ -54,6 +56,9 @@ const SIGNUP_CONTENT_MAX_W = 'max-w-[360px]'; // fields have more room; kept as a named constant to document the deliberate // difference from SIGNUP_CONTENT_MAX_W. const SIGNUP_FORM_MAX_W = 'max-w-[440px]'; +// The panel's column is as wide as the expanded form, not the marketing wall: +// the footer links and the disclaimer each need that width to stay on one line. +const SIGNUP_SPLIT_COLUMN_MAX_W = SIGNUP_FORM_MAX_W; export const OnboardingSignupHero = ({ children, @@ -80,6 +85,7 @@ export const OnboardingSignupHero = ({ const isSplitLayout = background === 'split'; const isDeskVariant = background === 'desk'; + const isPanelLayout = background === 'panel'; const showOrbsLayer = showOrbs; // Once the user moves to the email registration / verification step, drop the @@ -125,6 +131,104 @@ export const OnboardingSignupHero = ({ ); } + // Marketing-landing parity: the signup wall reuses the exact same elements as + // the other backgrounds (logo, headline, auth options, disclaimer, footer + // links); only the shell around them changes. Above `laptop` the form sits in + // a left column with the hero artwork framed in the right one. Below it the + // layout stacks: the artwork takes the top of the screen and dissolves into + // the page background, with the form bottom-anchored underneath. + if (isPanelLayout) { + const signupColumn = ( +
+ + + {headline && ( +

+ {headline} +

+ )} + + {children} +
+ ); + + return ( +
+