Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8c71702
feat(onboarding): redesign the post-signup funnel steps
tsahimatsliah Jul 29, 2026
05cdbd3
fix(onboarding): cap the feed preview grid, enlarge the top-bar chrome
tsahimatsliah Jul 30, 2026
93a4614
fix(onboarding): dial back the funnel logo size on desktop
tsahimatsliah Jul 30, 2026
c4e861b
fix(onboarding): set the funnel desktop logo to 24px
tsahimatsliah Jul 30, 2026
8a57cc9
fix(onboarding): keep the redesign inside the post-signup funnel
tsahimatsliah Jul 30, 2026
c331760
fix(onboarding): restore the paid funnel's layout, copy and background
tsahimatsliah Jul 30, 2026
08773ea
fix(onboarding): finish gating the redesign, and fix the reminder key…
tsahimatsliah Jul 30, 2026
11c8298
Merge remote-tracking branch 'origin/main' into claude/onboarding-but…
tsahimatsliah Jul 30, 2026
75001a6
fix(onboarding): style the signup screen the flow actually uses
tsahimatsliah Jul 30, 2026
7e42d6b
fix(onboarding): style the signup screen from the step that owns it
tsahimatsliah Jul 30, 2026
91a8830
feat(onboarding): give the account-details screen the funnel chrome
tsahimatsliah Jul 30, 2026
310776a
chore(onboarding): drop the 3-vs-4 feed preview comparison story
tsahimatsliah Jul 30, 2026
1ae65b5
Merge origin/main into the onboarding funnel redesign
tsahimatsliah Jul 30, 2026
baf3675
Merge branch 'main' into claude/onboarding-button-alignment-6e8571
tsahimatsliah Jul 30, 2026
e976cc5
fix(onboarding): address the review on the funnel redesign
tsahimatsliah Jul 30, 2026
1b264b0
Merge the branch update into the review fixes
tsahimatsliah Jul 30, 2026
d275a01
fix(onboarding): lighten the gradient and finish the funnel chrome on…
tsahimatsliah Jul 30, 2026
035b408
fix(onboarding): show the terms notice only where an account is created
tsahimatsliah Jul 30, 2026
3eab074
refactor(onboarding): drop the funnel background, leave the steps flat
tsahimatsliah Jul 30, 2026
9959d78
refactor(onboarding): tidy up the leftovers a second review pass found
tsahimatsliah Jul 31, 2026
c47869f
fix(onboarding): keep three more steps out of the paid funnel
tsahimatsliah Jul 31, 2026
2cb06ad
refactor: cut the commentary back to what the code can't say itself
tsahimatsliah Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/extension/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ module.exports = {
'\\.svg$': '<rootDir>/__mocks__/svgrMock.ts',
'\\.css$': 'identity-obj-proxy',
'react-markdown': '<rootDir>/__mocks__/reactMarkdownMock.tsx',
'^edge-aura/react$': '<rootDir>/../shared/__mocks__/edgeAuraMock.tsx',
},
};
8 changes: 7 additions & 1 deletion packages/extension/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react"
"jsx": "react",
"paths": {
// See packages/shared/tsconfig.json — this package compiles shared's
// sources under `moduleResolution: node`, which cannot follow the
// package `exports` subpath the bundler resolves at build time.
"edge-aura/react": ["../shared/node_modules/edge-aura/dist/react.d.ts"]
}
},
"include": [
"custom.d.ts",
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/__mocks__/edgeAuraMock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* `edge-aura` ships ESM only and paints into a 2D canvas context that jsdom
* does not implement. The aura is a decorative overlay with no role in any
* assertion, so tests render nothing in its place.
*/
export const EdgeAura = (): null => null;
1 change: 1 addition & 0 deletions packages/shared/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ module.exports = {
'\\.css$': 'identity-obj-proxy',
'react-markdown': '<rootDir>/__mocks__/reactMarkdownMock.tsx',
'react-turnstile': 'identity-obj-proxy',
'^edge-aura/react$': '<rootDir>/__mocks__/edgeAuraMock.tsx',
},
};
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
"@tiptap/starter-kit": "^3.22.5",
"check-password-strength": "^2.0.10",
"cmdk": "^1.0.0",
"edge-aura": "0.6.0",
"emojibase-data": "^17.0.0",
"fetch-event-stream": "^0.1.6",
"graphql-ws": "^5.5.5",
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/src/components/GrowthBookProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,7 @@ export const useFeature: GetFeatureValue = (feature) =>

export const useGrowthBookContext = (): GrowthBookContextValue =>
useContext(GrowthBookContext);

// Re-exported so harnesses (Storybook, tests) can pin an experiment arm without
// depending on the GrowthBook package directly.
export { GrowthBookContext };
32 changes: 32 additions & 0 deletions packages/shared/src/components/auth/AuthHeader.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import AuthHeader from './AuthHeader';
import { onboardingHeadlineClasses } from '../onboarding/common';

// `onboardingHeadline` exists so the two screens that sit in FRONT of the
// onboarding funnel (account details, verify email) can take the funnel's
// headline scale. It is deliberately separate from `simplified`, which eleven
// other surfaces set — including the paid /helloworld funnel and the recruiter
// sign-in modals — so these assert both directions.
describe('AuthHeader', () => {
it('takes the funnel headline scale when onboardingHeadline is set', () => {
render(
<AuthHeader simplified onboardingHeadline title="Verify your email" />,
);

const heading = screen.getByRole('heading', { name: 'Verify your email' });

onboardingHeadlineClasses.split(' ').forEach((className) => {
expect(heading).toHaveClass(className);
});
});

it('keeps the original simplified scale for every other surface', () => {
render(<AuthHeader simplified title="Sign up" />);

const heading = screen.getByRole('heading', { name: 'Sign up' });

expect(heading).toHaveClass('typo-title2');
expect(heading).not.toHaveClass('typo-title1');
});
});
15 changes: 14 additions & 1 deletion packages/shared/src/components/auth/AuthHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,24 @@ import { Button, ButtonVariant } from '../buttons/Button';
import { ArrowIcon } from '../icons';
import { Modal } from '../modals/common/Modal';
import { ModalHeaderKind } from '../modals/common/types';
import { onboardingHeadlineClasses } from '../onboarding/common';

export interface AuthHeaderProps extends ComponentProps<'h2'> {
simplified?: boolean;
/**
* Post-signup onboarding only: take the funnel's headline scale, so the
* signup and verify-email screens read as the same flow as the steps that
* follow them. Deliberately separate from `simplified`, which eleven other
* surfaces set — including the paid funnel and the recruiter modals.
*/
onboardingHeadline?: boolean;
title: string;
onBack?: (e: MouseEvent | KeyboardEvent | FormEvent) => void;
}

function AuthHeader({
simplified = false,
onboardingHeadline = false,
title,
className,
onBack,
Expand All @@ -28,7 +37,11 @@ function AuthHeader({
return (
<h2
{...attrs}
className="text-center font-bold text-text-primary typo-title2"
className={
onboardingHeadline
? onboardingHeadlineClasses
: 'text-center font-bold text-text-primary typo-title2'
}
>
{title}
</h2>
Expand Down
12 changes: 11 additions & 1 deletion packages/shared/src/components/auth/AuthOptionsInner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ function AuthOptionsInner({
ignoreMessages = false,
onboardingSignupButton,
hideLoginLink,
hideSignupDisclaimer,
isOnboardingFunnel,
compact,
splitSignupStyle,
preferGithub,
Expand Down Expand Up @@ -783,6 +785,7 @@ function AuthOptionsInner({
formRef={formRef}
simplified={simplified}
showHeadline={!hideRegistrationHeadline}
isOnboardingFunnel={isOnboardingFunnel}
extraFields={registrationExtraFields}
hints={registrationHints}
onBack={
Expand Down Expand Up @@ -856,6 +859,7 @@ function AuthOptionsInner({
className={className}
onboardingSignupButton={onboardingSignupButton}
hideLoginLink={hideLoginLink}
hideSignupDisclaimer={hideSignupDisclaimer}
compact={compact}
splitSignupStyle={splitSignupStyle}
preferGithub={preferGithub}
Expand All @@ -876,6 +880,7 @@ function AuthOptionsInner({
onProviderClick={onProviderClick}
isProviderLoading={isSocialAuthLoading}
simplified={simplified}
isOnboardingFunnel={isOnboardingFunnel}
onShowLoginOptions={() => {
if (!isLoginFlow && onAuthStateUpdate) {
onAuthStateUpdate({ isLoginFlow: true });
Expand Down Expand Up @@ -915,8 +920,13 @@ function AuthOptionsInner({
</Tab>
<Tab label={AuthDisplay.EmailVerification}>
<MailIcon size={IconSize.XXLarge} className="mx-auto mb-2" />
<AuthHeader simplified={simplified} title="Verify your email" />
<AuthHeader
simplified={simplified}
onboardingHeadline={isOnboardingFunnel}
title="Verify your email"
/>
<EmailCodeVerification
isOnboardingFunnel={isOnboardingFunnel}
onSubmit={onProfileSuccess}
onVerifyCode={async (code) => {
const res = await betterAuthVerifyEmailOTP(email, code);
Expand Down
8 changes: 7 additions & 1 deletion packages/shared/src/components/auth/AuthSignBack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface AuthSignBackProps extends AuthFormProps {
isProviderLoading?: boolean;
loginFormProps?: LoginFormProps;
onShowLoginOptions?: () => void;
isOnboardingFunnel?: boolean;
}

export const AuthSignBack = ({
Expand All @@ -32,6 +33,7 @@ export const AuthSignBack = ({
simplified,
onShowLoginOptions,
isConnectedAccount,
isOnboardingFunnel,
}: AuthSignBackProps): ReactElement | null => {
const { signBack, provider, isLoaded } = useSignBack();
const socialProvider =
Expand Down Expand Up @@ -107,7 +109,11 @@ export const AuthSignBack = ({

return (
<span className="flex flex-1 flex-col">
<AuthHeader simplified={simplified} title="Welcome back!" />
<AuthHeader
simplified={simplified}
onboardingHeadline={isOnboardingFunnel}
title="Welcome back!"
/>
<AuthContainer className="items-center">
<p className="mb-2 text-center text-text-secondary typo-callout">
Log in to access your account
Expand Down
31 changes: 23 additions & 8 deletions packages/shared/src/components/auth/EmailCodeVerification.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { ReactElement } from 'react';
import React, { useRef, useState } from 'react';
import classNames from 'classnames';
import { Button, ButtonVariant } from '../buttons/Button';
import { Button, ButtonSize, ButtonVariant } from '../buttons/Button';
import ConditionalWrapper from '../ConditionalWrapper';
import {
FunnelGlassBar,
funnelGlassBarCta,
} from '../../features/onboarding/shared/FunnelGlassBar';
import type { AuthFormProps } from './common';
import AuthForm from './AuthForm';
import { AuthEventNames } from '../../lib/auth';
Expand All @@ -22,6 +27,7 @@ interface EmailCodeVerificationProps extends AuthFormProps {
className?: string;
onVerifyCode?: (code: string) => Promise<void>;
onResendCode?: () => Promise<void>;
isOnboardingFunnel?: boolean;
}

const noop = (): void => undefined;
Expand All @@ -32,6 +38,7 @@ function EmailCodeVerification({
className,
onVerifyCode,
onResendCode,
isOnboardingFunnel,
}: EmailCodeVerificationProps): ReactElement {
const { email } = useAuthData();
const { logEvent } = useLogContext();
Expand Down Expand Up @@ -170,14 +177,22 @@ function EmailCodeVerification({
</button>
</span>
</div>
<Button
className="w-full"
type="submit"
variant={ButtonVariant.Primary}
loading={isVerifying}
{/* Same glass bar as the funnel steps and account details, so the last
screen before the funnel opens is not the one bare button in the flow. */}
<ConditionalWrapper
condition={!!isOnboardingFunnel}
wrapper={(component) => <FunnelGlassBar>{component}</FunnelGlassBar>}
>
Verify
</Button>
<Button
className={isOnboardingFunnel ? funnelGlassBarCta : 'w-full'}
type="submit"
size={isOnboardingFunnel ? ButtonSize.Medium : undefined}
variant={ButtonVariant.Primary}
loading={isVerifying}
>
Verify
</Button>
</ConditionalWrapper>
</AuthForm>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface OnboardingRegistrationFormProps extends AuthFormProps {
className?: ClassName;
onboardingSignupButton?: ButtonProps<'button'>;
hideLoginLink?: boolean;
hideSignupDisclaimer?: boolean;
compact?: boolean;
splitSignupStyle?: boolean;
preferGithub?: boolean;
Expand Down Expand Up @@ -115,6 +116,7 @@ export const OnboardingRegistrationForm = ({
trigger,
onboardingSignupButton,
hideLoginLink,
hideSignupDisclaimer,
compact,
splitSignupStyle = false,
preferGithub,
Expand Down Expand Up @@ -223,7 +225,7 @@ export const OnboardingRegistrationForm = ({
/>
);

const disclaimer = (
const disclaimer = hideSignupDisclaimer ? null : (
<SignupDisclaimer className="!text-text-tertiary tablet:!typo-footnote" />
);

Expand Down
45 changes: 36 additions & 9 deletions packages/shared/src/components/auth/RegistrationFieldsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { TextField } from '../fields/TextField';
import { PasswordField } from '../fields/PasswordField';
import { Checkbox } from '../fields/Checkbox';
Expand All @@ -13,6 +14,7 @@ import { Button, ButtonVariant } from '../buttons/Button';
import ImageInput from '../fields/ImageInput';
import { useGenerateUsername } from '../../hooks';
import { labels } from '../../lib';
import { AtIcon, MailIcon, UserIcon } from '../icons';

export type UserExperienceLevelKey = keyof typeof UserExperienceLevel;

Expand Down Expand Up @@ -42,6 +44,8 @@ export interface RegistrationFieldsFormProps {
// Optional extra profile fields to render, driven by the onboarding funnel
// (campaign cohorts). Empty/undefined renders the default fields only.
extraFields?: ProfileExtraField[];
/** Suppresses the form's own submit button, for a docked `<button form={id}>`. */
formId?: string;
}

const RegistrationFieldsForm: React.FC<RegistrationFieldsFormProps> = ({
Expand All @@ -53,6 +57,7 @@ const RegistrationFieldsForm: React.FC<RegistrationFieldsFormProps> = ({
onResetErrors,
withPassword,
extraFields = [],
formId,
}) => {
const [values, setValues] = useState<FormValues>({
email: initialValues.email || '',
Expand Down Expand Up @@ -138,7 +143,15 @@ const RegistrationFieldsForm: React.FC<RegistrationFieldsFormProps> = ({
};

return (
<form onSubmit={handleSubmit} className="flex flex-col items-center gap-2">
<form
id={formId}
onSubmit={handleSubmit}
className={classNames(
'flex flex-col items-center',
// `formId` doubles as the marker for the funnel's roomier field stack.
formId ? 'w-full gap-4' : 'gap-2',
)}
>
{initialValues.image && (
<ImageInput
className={{ container: 'mb-4' }}
Expand All @@ -151,6 +164,9 @@ const RegistrationFieldsForm: React.FC<RegistrationFieldsFormProps> = ({
name="email"
inputId="email"
label="Email"
leftIcon={
formId ? <MailIcon aria-hidden role="presentation" /> : undefined
}
type="email"
value={values.email}
onChange={handleChange('email')}
Expand All @@ -166,6 +182,9 @@ const RegistrationFieldsForm: React.FC<RegistrationFieldsFormProps> = ({
/>
<TextField
name="name"
leftIcon={
formId ? <UserIcon aria-hidden role="presentation" /> : undefined
}
inputId="name"
label="Name"
value={values.name}
Expand Down Expand Up @@ -202,6 +221,9 @@ const RegistrationFieldsForm: React.FC<RegistrationFieldsFormProps> = ({
)}
<TextField
name="username"
leftIcon={
formId ? <AtIcon aria-hidden role="presentation" /> : undefined
}
inputId="username"
label="Enter a username"
value={inputUsername}
Expand Down Expand Up @@ -277,6 +299,9 @@ const RegistrationFieldsForm: React.FC<RegistrationFieldsFormProps> = ({
/>
)}
<Checkbox
// An inline-flex label would otherwise shrink to its text and float to
// the middle of the centred form, off the fields' left edge.
className={formId ? 'mt-2 w-full' : undefined}
name="optOutMarketing"
checked={values.optOutMarketing}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -286,14 +311,16 @@ const RegistrationFieldsForm: React.FC<RegistrationFieldsFormProps> = ({
>
I don&apos;t want to receive updates and promotions via email
</Checkbox>
<Button
className="w-full"
type="submit"
variant={ButtonVariant.Primary}
disabled={disabled}
>
{submitLabel}
</Button>
{!formId && (
<Button
className="w-full"
type="submit"
variant={ButtonVariant.Primary}
disabled={disabled}
>
{submitLabel}
</Button>
)}
</form>
);
};
Expand Down
Loading
Loading