Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 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
bb88e01
test(e2e): cover the single-page onboarding flow
talissoncosta Jun 24, 2026
fd28698
fix(onboarding): send new users to the onboarding flow, not /create
talissoncosta Jun 25, 2026
04a7b84
test(e2e): wait for the post-signup redirect instead of navigating
talissoncosta Jun 25, 2026
bfa5cd6
fix(onboarding): make the flag toggle optimistic
talissoncosta Jun 25, 2026
b57ee74
fix(onboarding): lock the flag toggle until the app connects
talissoncosta Jun 25, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Meta, StoryObj } from 'storybook'

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' }] }],
},
}
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 },
}
113 changes: 113 additions & 0 deletions frontend/e2e/tests/onboarding-tests.pw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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) a new user lands
// on at /getting-started. Mirror of the legacy signup test's guard: this runs
// only when the flag is on, the legacy signup test runs only when it's off.
//
// Selectors are accessibility-first (roles / labels / text), not data-test ids:
// the header inputs expose aria-labels, the copy buttons and the flag switch
// carry accessible names, and the flags table is a labelled region.
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 flow renders once bootstrap settles (it shows a loader, then an error
// heading on failure - so the welcome heading means "ready").
const flowReady = async () =>
page
.getByRole('heading', { name: /Welcome/ })
.waitFor({ state: 'visible', timeout: 30000 });

// Sign up a fresh user; with the flag on, a getting-started user is routed
// to /getting-started where the flow bootstraps the org / project / flag.
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 here races the post-signup auth and gets
// bounced to /?redirect=. The app redirects a getting-started user to the
// flow itself once authenticated, so just wait for it to land there.
log('Land on the onboarding flow');
await page.waitForURL((url) => url.pathname === '/getting-started', {
timeout: 30000,
});
await flowReady();
await visualSnapshot(page, 'onboarding-flow', testInfo);

// The verify terminal starts pre-connection: LISTENING, nothing ticked.
await expect(page.getByText('LISTENING')).toBeVisible();
await expect(page.getByText('Copy install command')).not.toContainText('✓');

// Copying the install + wire snippets ticks the checklist (the visible
// [✓] prefix is the done state).
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('✓');

// The flags table is locked until the app connects, and there's no real
// first evaluation in a test - so force the connected state via ?connected
// (the stub seam for #7767). The badge flips to LIVE and the toggle unlocks.
log('Force the connected state');
await page.goto('/getting-started?connected');
await flowReady();
await expect(page.getByText('LIVE', { exact: true })).toBeVisible();

// Now the Development toggle is enabled. 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');

// Reload to prove the rename persisted server-side and the tag came with it
// (bootstrap is idempotent and reuses the renamed flag on revisit).
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

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 ?

) {
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 @@ -10,11 +10,23 @@ export type CodeCardProps = {
language: string
// Left side of the card header (e.g. the language label or npm/yarn pills).
headerLeft: ReactNode
// Fires when the user copies the code (drives the verify checklist).
onCopy?: () => void
// Accessible name for the copy button. The visible label is the same ("Copy
// Code") on every card, so this distinguishes them for screen readers;
// defaults to the visible label.
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 +37,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 @@ -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