Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6d88a66
feat(onboarding): add the verify terminal (#7766)
talissoncosta Jun 22, 2026
d192575
feat(onboarding): add the flags table (#7766)
talissoncosta Jun 22, 2026
cdd1cb7
feat(onboarding): render terminal + flags table with a real Dev toggl…
talissoncosta Jun 22, 2026
35b5022
style(onboarding): radius tokens + restore the connected card's purpl…
talissoncosta Jun 22, 2026
eb2725e
refactor(onboarding): route connection status through useOnboardingCo…
talissoncosta Jun 22, 2026
202b3c5
feat(onboarding): show the flag's real tags in the table (#7766)
talissoncosta Jun 22, 2026
82e4046
feat(onboarding): attach an Onboarding tag to the demo flag in bootst…
talissoncosta Jun 22, 2026
dbd2433
feat(onboarding): tick the verify checklist as the user copies the sn…
talissoncosta Jun 22, 2026
fe9913b
style(onboarding): align the flags toggle under the ENABLED header (#…
talissoncosta Jun 22, 2026
96e125e
refactor(onboarding): use a tag-palette colour for the Onboarding tag…
talissoncosta Jun 23, 2026
d2474f9
fix(forms): disable browser autofill on GhostInput
talissoncosta Jun 23, 2026
63459ee
fix(onboarding): toast on flag toggle failure
talissoncosta Jun 23, 2026
815647d
docs(onboarding): note the verify checklist is session-only
talissoncosta Jun 23, 2026
880188a
refactor(onboarding): use Bootstrap/token utilities for layout and su…
talissoncosta Jun 24, 2026
36c1a9e
feat(onboarding): accessible names for the copy buttons and flag switch
talissoncosta Jun 25, 2026
79ae58f
fix(onboarding): preserve tags when renaming the flag
talissoncosta Jun 24, 2026
28aef22
fix(onboarding): send new users to the onboarding flow, not /create
talissoncosta Jun 25, 2026
54ac8c7
fix(onboarding): make the flags-table toggle reliable
talissoncosta Jun 25, 2026
547a18b
test(e2e): cover the single-page onboarding flow
talissoncosta Jun 25, 2026
139ced3
refactor(onboarding): tidy per review
talissoncosta Jun 25, 2026
13bfa10
fix(onboarding): drop the missing mono token from the connect panel too
talissoncosta Jun 25, 2026
2e3060d
refactor(onboarding): extract togglesLocked for a readable disabled c…
talissoncosta Jun 25, 2026
5e78567
docs(types): trim the isGettingStarted comment to the non-obvious bit
talissoncosta Jun 26, 2026
10f5c37
docs(onboarding): trim comments to the load-bearing why
talissoncosta Jun 26, 2026
f9b0ac5
fix(onboarding): match the onboarding flag by its tag, not the first …
talissoncosta Jun 26, 2026
2fe5a1e
refactor(onboarding): type flag-table tags as TTag[], not Partial
talissoncosta Jun 26, 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
2 changes: 2 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,8 @@ export type User = {
last_login: string
uuid: string
onboarding: Onboarding
// Set client-side at login, not returned by the API.
isGettingStarted?: boolean
// TODO: Use enum
role: string
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from 'storybook'

import { Tag } from 'common/types/responses'
import OnboardingFlagsTable, {
OnboardingFlagRow,
} from 'components/pages/onboarding/OnboardingFlagsTable'

const demoFlag: OnboardingFlagRow = {
description: 'Controls the demo button shown to your users',
enabled: true,
name: 'show_demo_button',
}

const meta: Meta<typeof OnboardingFlagsTable> = {
args: {
flags: [demoFlag],
onToggle: () => {},
status: 'connected',
},
component: OnboardingFlagsTable,
parameters: {
docs: {
description: {
component:
'The "Your flags" card from the onboarding flow, reusing the product FeatureName / Tag / Switch. Prop-driven: the page owns the flag data and the persisted Dev toggle. `connected` lifts the card with the accent border and glow; `waiting` dims it until the first evaluation arrives.',
},
},
layout: 'padded',
},
title: 'Pages/Onboarding/OnboardingFlagsTable',
}
export default meta

type Story = StoryObj<typeof OnboardingFlagsTable>

export const Connected: Story = {}

export const Waiting: Story = {
args: { status: 'waiting' },
}

export const Off: Story = {
args: { flags: [{ ...demoFlag, enabled: false }] },
}

export const WithTag: Story = {
args: {
flags: [
{ ...demoFlag, tags: [{ color: '#6837FC', label: 'demo' } as Tag] },
],
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Meta, StoryObj } from 'storybook'

import OnboardingTerminal from 'components/pages/onboarding/OnboardingTerminal'

const meta: Meta<typeof OnboardingTerminal> = {
args: {
connected: false,
featureName: 'show_demo_button',
installCopied: false,
snippetCopied: false,
},
component: OnboardingTerminal,
parameters: {
docs: {
description: {
component:
'The onboarding verify console. The checklist ticks as the user acts (copy install, copy snippet), and the first evaluation flips the badge to LIVE and prints the connection receipt. Always dark, since a terminal reads the same in light and dark mode.',
},
},
layout: 'padded',
},
title: 'Pages/Onboarding/OnboardingTerminal',
}
export default meta

type Story = StoryObj<typeof OnboardingTerminal>

export const Listening: Story = {}

export const InstallCopied: Story = {
args: { installCopied: true },
}

export const SnippetsCopied: Story = {
args: { installCopied: true, snippetCopied: true },
}

export const Connected: Story = {
args: { connected: true, installCopied: true, snippetCopied: true },
}
105 changes: 105 additions & 0 deletions frontend/e2e/tests/onboarding-tests.pw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { test, expect } from '../test-setup';
import { byId, createHelpers, getFlagsmith, log, visualSnapshot } from '../helpers';
import { E2E_SIGN_UP_USER, PASSWORD } from '../config';

// The single-page onboarding flow (onboarding_quickstart_flow), at
// /getting-started. Runs only when the flag is on (the legacy signup test runs
// when it's off).
test.describe('Onboarding', () => {
test('New user connects via the single-page onboarding flow @oss', async ({
page,
}, testInfo) => {
const { addErrorLogging, click, setText, waitForElementVisible } =
createHelpers(page);
const flagsmith = await getFlagsmith();

test.skip(
!flagsmith.hasFeature('onboarding_quickstart_flow'),
'Onboarding flow is behind onboarding_quickstart_flow',
);

await addErrorLogging();

// The welcome heading means bootstrap settled (loader, then heading/error).
const flowReady = async () =>
page
.getByRole('heading', { name: /Welcome/ })
.waitFor({ state: 'visible', timeout: 30000 });

log('Sign up');
await page.goto('/');
await click(byId('jsSignup'));
await waitForElementVisible(byId('firstName'));
await setText(byId('firstName'), 'Bullet');
await setText(byId('lastName'), 'Train');
await setText(byId('email'), E2E_SIGN_UP_USER);
await setText(byId('password'), PASSWORD);
await click(byId('signup-btn'));

// Don't navigate manually - a goto races the post-signup auth and bounces to
// /?redirect=. The app routes a getting-started user here itself, so just wait.
log('Land on the onboarding flow');
await page.waitForURL((url) => url.pathname === '/getting-started', {
timeout: 30000,
});
await flowReady();
await visualSnapshot(page, 'onboarding-flow', testInfo);

await expect(page.getByText('LISTENING')).toBeVisible();
await expect(page.getByText('Copy install command')).not.toContainText('✓');

log('Copy snippets, checklist ticks');
await page.getByRole('button', { name: 'Copy install command' }).click();
await expect(page.getByText('Copy install command')).toContainText('✓');
await page.getByRole('button', { name: 'Copy code snippet' }).click();
await expect(page.getByText('Copy code snippet')).toContainText('✓');

// No real first evaluation in a test, so force the connected state via
// ?connected (the #7767 stub); that unlocks the toggle and flips LIVE.
log('Force the connected state');
await page.goto('/getting-started?connected');
await flowReady();
await expect(page.getByText('LIVE', { exact: true })).toBeVisible();

// Two switches on the page (theme + flag), so scope to the flags region.
log('Toggle the flag');
const flagsTable = page.getByRole('region', { name: 'Your flags' });
const flagSwitch = flagsTable.getByRole('switch');
await flagSwitch.waitFor({ state: 'visible' });
const wasChecked = (await flagSwitch.getAttribute('class'))?.includes(
'switch-checked',
);
await flagSwitch.click();
await expect(flagSwitch).toHaveClass(
wasChecked ? /switch-unchecked/ : /switch-checked/,
);

// The Onboarding badge (attached in bootstrap) shows in the flags table.
// Exact match: the header crumb also contains the word "Onboarding".
await expect(flagsTable.getByText('Onboarding', { exact: true })).toBeVisible();

// Rename the flag. Names are immutable, so this delete + recreates; the
// Onboarding tag must survive (the recreate carries the old flag's tags).
log('Rename the flag');
const flagInput = page.getByLabel('Flag name');
await flagInput.fill('renamed_demo_flag');
await flagInput.press('Enter');

// Wait for the rename to persist before reloading - it's a delete + recreate,
// and reloading too early aborts the requests. The toast fires once both land.
await expect(page.getByText('Flag name updated')).toBeVisible({
timeout: 20000,
});

// Reload to prove it persisted (bootstrap reuses the renamed flag).
await page.reload();
await flowReady();
await expect(page.getByLabel('Flag name')).toHaveValue('renamed_demo_flag');
await expect(
page
.getByRole('region', { name: 'Your flags' })
.getByText('Onboarding', { exact: true }),
).toBeVisible();
await visualSnapshot(page, 'onboarding-renamed', testInfo);
});
});
13 changes: 11 additions & 2 deletions frontend/web/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,17 @@ const App = class extends Component {
}

if (!AccountStore.getOrganisation() && !invite) {
// If user has no organisation redirect to /create
this.props.history.replace(`/create${query}`)
// New users with no organisation go through the single-page onboarding
// flow when it's enabled - it creates the organisation itself, so it
// replaces the legacy /create page. Everyone else still gets /create.
if (
AccountStore.getUser()?.isGettingStarted &&

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.

I'm not sure this has been added to the User type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You are right, it wasn't. Added isGettingStarted to the User type (optional, since it's set client-side at login rather than returned by the API). 139ced3.

Utils.getFlagsmithHasFeature('onboarding_quickstart_flow')

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.

Should we also gate L190 with the flag ?

@talissoncosta talissoncosta Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As far as I've tested, this is the right place to gate it. A fresh signup has no org yet, so it hits this branch first and gets redirected to /getting-started straight away. Happy to dig onto it if you really think it is needed.

) {
this.props.history.replace('/getting-started')
} else {
this.props.history.replace(`/create${query}`)
}
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ const GhostInput = ({
onKeyDown={onKeyDown}
aria-label={ariaLabel}
spellCheck={false}
// Opt out of browser autofill + password-manager overlays (1Password,
// LastPass); their icons would overlap the trailing edit pencil.
autoComplete='off'
data-1p-ignore
data-lpignore='true'
style={{ width: inputWidth }}
/>
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@ import { useCopyFeedback } from 'components/pages/onboarding/hooks/useCopyFeedba

export type CodeCardProps = {
code: string
// Syntax language for the body's highlighting (e.g. 'bash', 'javascript').
// highlight.js language for the body.
language: string
// Left side of the card header (e.g. the language label or npm/yarn pills).
headerLeft: ReactNode
// Drives the verify checklist.
onCopy?: () => void
// Accessible name; the visible "Copy Code" label is identical on every card.
copyLabel?: string
}

// Owns its own "Copied" feedback so each card is independent. Highlight escapes
// the body for display; Copy uses the raw string.
const CodeCard: FC<CodeCardProps> = ({ code, headerLeft, language }) => {
const CodeCard: FC<CodeCardProps> = ({
code,
copyLabel,
headerLeft,
language,
onCopy,
}) => {
const { copied, copy } = useCopyFeedback()

return (
Expand All @@ -25,7 +32,11 @@ const CodeCard: FC<CodeCardProps> = ({ code, headerLeft, language }) => {
theme='primary'
size='small'
className='ms-auto'
onClick={() => copy(code)}
aria-label={copyLabel}
onClick={() => {
copy(code)
onCopy?.()
}}
>
<span
className='d-inline-flex align-items-center gap-1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@ const PACKAGE_MANAGERS: PackageManager[] = ['npm', 'yarn']
export type ConnectYourCodePanelProps = {
environmentKey: string
featureName: string
onCopyInstall?: () => void
onCopyWire?: () => void
}

// "Connect your code" tab: pick an SDK, then copy the install + wire snippets,
// pre-filled with the real env key and flag this onboarding created.
// pre-filled with the real env key and flag this onboarding created. The copy
// actions feed the verify checklist.
const ConnectYourCodePanel: FC<ConnectYourCodePanelProps> = ({
environmentKey,
featureName,
onCopyInstall,
onCopyWire,
}) => {
const [sdkLang, setSdkLang] = useState<SdkLang>(SDK_LANGS[0])
const [installPm, setInstallPm] = useState<PackageManager>('npm')
Expand All @@ -40,6 +45,8 @@ const ConnectYourCodePanel: FC<ConnectYourCodePanelProps> = ({
<CodeCard
code={installCode}
language='bash'
onCopy={onCopyInstall}
copyLabel='Copy install command'
headerLeft={
sdkSnippet.installYarn ? (
<div className='onboarding-connect__pm d-inline-flex'>
Expand Down Expand Up @@ -74,6 +81,8 @@ const ConnectYourCodePanel: FC<ConnectYourCodePanelProps> = ({
<CodeCard
code={sdkSnippet.wire}
language={sdkSnippet.language}
onCopy={onCopyWire}
copyLabel='Copy code snippet'
headerLeft={
<span className='onboarding-connect__codecard-lang'>
{sdkLang.label}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

&__prompt-text {
color: var(--color-text-default);
font-family: var(--font-family-mono, monospace);
font-family: ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace;
font-size: 0.8125rem;
line-height: 1.5;
background: transparent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const CONNECT_TABS: OnboardingTab<ConnectTab>[] = [
export type OnboardingConnectPanelProps = {
environmentKey: string
featureName: string
onCopyInstall?: () => void
onCopyWire?: () => void
}

// Two ways to connect an app to the pre-created flag: paste an agent-agnostic
Expand All @@ -37,6 +39,8 @@ export type OnboardingConnectPanelProps = {
const OnboardingConnectPanel: FC<OnboardingConnectPanelProps> = ({
environmentKey,
featureName,
onCopyInstall,
onCopyWire,
}) => {
const [tab, setTab] = useState<ConnectTab>('manual')

Expand Down Expand Up @@ -68,6 +72,8 @@ const OnboardingConnectPanel: FC<OnboardingConnectPanelProps> = ({
<ConnectYourCodePanel
environmentKey={environmentKey}
featureName={featureName}
onCopyInstall={onCopyInstall}
onCopyWire={onCopyWire}
/>
</OnboardingTabPanel>
</div>
Expand Down
Loading
Loading