feat: onboarding prototype#7637
Conversation
Folder/identifier rename from "onboarding-aha" to "onboarding-quickstart" matches the design intent (fast path to first eval) rather than the internal product term. Includes the SCSS class prefix, the feature flag name (`onboarding_quickstart_flow`), the page component name, and the track-event namespace. New shape adds a role-selection step at the start (Engineer / PM / Other) and branches the flow: - Engineer: SDK install snippet + first-eval polling (existing) - PM: integrations grid (visual capability check) + invite-an-engineer - Other: skips the evaluation step entirely, drops to features page Other changes folded in: - CodeSnippet rebuilt with correct package names (`@Flagsmith/flagsmith`), minimal per-language snippets, and interpolation of the user's chosen feature name (not placeholder identifiers) - Custom toggle replaced with the existing `Switch` component - Skip button consolidated to the page header (was duplicated in each step footer) - "Choose for me" buttons removed from Org + Project steps — the pre-fill via `useSmartDefaults` does the same job - Per-block layout constraints (form steps narrow, evaluation step wide, page itself full-width) — drops the legacy 1280px page cap - text-secondary → text-muted across the flow to avoid the Bootstrap `$secondary` (#fae392) collision that fails contrast on light surfaces - "Aha" terminology dropped from step names — internal key `'evaluation'`, user-facing title "See it works" The activation signal (real first-SDK-eval detection) is still the polling stub from earlier — replacement signal needs to pull from Influx, tracked separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keyboard accessibility:
- Enter on org / project / custom-feature inputs advances the step
- Step transition focuses the first interactive element of the new step
- RoleStep and FeatureStep now follow WAI-ARIA radiogroup pattern with
arrow-key navigation (ArrowUp/Down/Left/Right + Home/End), roving
tabindex, and `role='radio'` / `aria-checked` semantics
- First arrow press with no selection picks the focused option
instead of skipping past it
- Enter on a focused option (with a valid selection) advances —
saves a Tab + Enter to reach the Next/Finish button
- Visible `:focus-visible` outline restored within `.onboarding-quickstart`,
scoped to override the project-wide `.btn:focus-visible { box-shadow: none }`
rule that hides focus for sighted keyboard users
PM path content:
- "See it works" stepper label is role-aware: engineer keeps it,
PM gets "Connect your tools"
- PM evaluation step now shows a read-only integrations grid (visual
capability check) using the same data merge as `IntegrationSelect`
(Flagsmith remote-config `integration_data` + `Constants.integrationSummaries`,
deduped by title). Top 12 entries rendered as cards
- Reuse-the-whole `IntegrationSelect` was considered but discarded —
its select-tools interaction has no downstream effect today, which
would set wrong expectations at the AHA moment
- PM CTA copy: "Invite a teammate" (was "Invite an engineer")
New primitive:
- `BareButton` — a `<button>` reset for keyboard-accessible custom
surfaces (card rows, custom radios, icon-only triggers). Defaults
`type='button'`, provides a focus-visible outline. SCSS co-located.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Code Review
This pull request introduces a new multi-step onboarding quickstart flow (AHA-first flow) gated by a feature flag, featuring custom wizard steps tailored to user roles, a slide-out progress drawer, and minimal SDK code snippets for various languages. While the implementation is clean and well-structured, several critical issues need to be addressed: unsafe JSON parsing of remote-config integration data could crash the flow for PM users; syntax errors in the generated C# code snippet will cause compiler errors; the FORCE_ON flag remains hardcoded to true; and the final redirection URL incorrectly uses display names and API keys instead of project and environment IDs.
| const pmIntegrations = useMemo(() => { | ||
| if (role !== 'pm') return [] | ||
| const remote = Utils.getFlagsmithValue('integration_data') | ||
| if (typeof remote !== 'string' || !remote) { | ||
| return Constants.integrationSummaries.slice(0, 12) | ||
| } | ||
| const merged = uniqBy( | ||
| Object.values(JSON.parse(remote)).concat( | ||
| Constants.integrationSummaries, | ||
| ) as IntegrationSummary[], | ||
| (v) => v.title.toLowerCase(), | ||
| ) | ||
| return merged.slice(0, 12) | ||
| }, [role]) |
There was a problem hiding this comment.
The parsing of the remote-config value integration_data is done using JSON.parse and Object.values without any safety guards or error handling. If integration_data is malformed, null, or not a dictionary, this will throw a runtime exception and completely crash the onboarding flow for PM users.
Wrap the parsing logic in a try...catch block and validate that the parsed result is an object/array before calling Object.values or concatenating.
const pmIntegrations = useMemo(() => {
if (role !== 'pm') return []
const remote = Utils.getFlagsmithValue('integration_data')
if (typeof remote !== 'string' || !remote) {
return Constants.integrationSummaries.slice(0, 12)
}
try {
const parsed = JSON.parse(remote)
if (!parsed || typeof parsed !== 'object') {
return Constants.integrationSummaries.slice(0, 12)
}
const remoteValues = Array.isArray(parsed) ? parsed : Object.values(parsed)
const merged = uniqBy(
remoteValues.concat(
Constants.integrationSummaries,
) as IntegrationSummary[],
(v) => v.title?.toLowerCase(),
)
return merged.slice(0, 12)
} catch (e) {
// eslint-disable-next-line no-console
console.error('Failed to parse integration_data', e)
return Constants.integrationSummaries.slice(0, 12)
}
}, [role])
| case 'dotnet': | ||
| return `var flagsmith = new FlagsmithClient(new FlagsmithConfiguration { | ||
| EnvironmentKey = "${environmentKey}" | ||
| }); | ||
| var flags = await flagsmith.GetEnvironmentFlags(); | ||
| Console.WriteLine($"${featureName}: {await flags.IsFeatureEnabled(\\"${featureName}\\")}");` |
There was a problem hiding this comment.
There are two issues in the generated C# (dotnet) code snippet that will cause compiler errors:
- The first
$is escaped as\$, which outputs literal${featureName}in the C# code. SincefeatureNameis not a defined variable in the C# scope, this will fail to compile. It should be unescaped so that the actual feature name is interpolated at generation time. - The double quotes inside the interpolation hole
{}are escaped as\". In C# interpolated strings, escaping quotes inside the expression hole is a syntax error. They should be unescaped double quotes".
Let's correct the snippet to generate valid, compilable C# code.
| case 'dotnet': | |
| return `var flagsmith = new FlagsmithClient(new FlagsmithConfiguration { | |
| EnvironmentKey = "${environmentKey}" | |
| }); | |
| var flags = await flagsmith.GetEnvironmentFlags(); | |
| Console.WriteLine($"${featureName}: {await flags.IsFeatureEnabled(\\"${featureName}\\")}");` | |
| case 'dotnet': | |
| return `var flagsmith = new FlagsmithClient(new FlagsmithConfiguration { | |
| EnvironmentKey = "${environmentKey}" | |
| }); | |
| var flags = await flagsmith.GetEnvironmentFlags(); | |
| Console.WriteLine($"${featureName}: {await flags.IsFeatureEnabled("${featureName}")}");` |
|
|
||
| // TEMP: forced on for local validation. Revert to `Utils.getFlagsmithHasFeature(...)` | ||
| // before merge so the flag actually gates the flow. | ||
| const FORCE_ON = true |
There was a problem hiding this comment.
The FORCE_ON flag is hardcoded to true for local validation. As noted in the comments and PR description, this needs to be reverted to use the actual feature flag check before merging to production to avoid exposing the unfinished flow prematurely.
| const FORCE_ON = true | |
| const FORCE_ON = false |
| const finishedDestination = | ||
| environmentKey && projectName | ||
| ? `/project/${projectName}/environment/${environmentKey}/features` | ||
| : '/organisations' |
There was a problem hiding this comment.
The finishedDestination URL is constructed using projectName and environmentKey (which is the API key) as path parameters:
/project/${projectName}/environment/${environmentKey}/features
However, the application routes (defined in routes.js) expect numeric/alphanumeric IDs for :projectId and :environmentId (e.g., /project/:projectId/environment/:environmentId/features). Using display names or API keys in the URL path will cause routing or API fetch failures.
Please ensure that the real implementation uses the actual projectId and environmentId instead of the display name and API key.
- Rename role title from "Something else" to "Other" - Drop the evaluation step from the stepper when Other is picked — they never reach it, so it shouldn't appear on the timeline - Make finishing as Other actually land on the features page; previously the stale environmentKey closure routed them back to /organisations Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…feature steps - Show 'My first project' as placeholder text instead of pre-filled content, falling back to it when the field is left blank. - Disallow spaces in the custom feature name, mirroring the main app's feature-ID constraint (spaces -> underscores). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nment creation Adds createOrganisation, createProject and createEnvironment RTK Query mutations — previously these only existed in the legacy Flux stores (account-store / organisation-store). These are the foundation for wiring the onboarding create chain to real APIs without leaning on the stores the project is migrating off. Also documents the backend integration plan for the flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the DEMO_ENVIRONMENT_KEY stub with the real create chain: reuse-or-create organisation -> create project -> create Development + Production environments -> create the first feature flag, then land on the project's features page. - Organisation is hybrid: reuse the already-selected org (common after signup) and skip the org step; create + select one only when none exists. Org selection state is Flux/Redux-owned, so a pure-RTK create would leave the shell unaware of the new org. - Fixes the features URL to use the numeric project id and the environment api_key (was wrongly using the project name as a slug). - Step navigation is now array-driven so it stays correct as steps are added/removed (org skip, role branching). - Per-step error handling surfaces failures via ErrorMessage. - Refreshes the legacy organisation store so the shell picks up the new project without a reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… org-create coherence When onboarding_quickstart_flow is enabled, post-signup users with no organisation are routed to /getting-started (the new flow creates the org as its first step) instead of the legacy /create page. Flag off = unchanged /create behaviour. Also fixes the create-org path to go through the account store (AppActions.createOrganisation + select) rather than the RTK mutation. Much of the shell still reads the current organisation from AccountStore, which a pure-RTK create leaves empty — verified end-to-end that this caused org-scoped calls to fire with undefined/garbage ids. Routing via the account store populates it so the new org id flows correctly. Verified on staging: fresh signup -> /getting-started -> create org -> project -> Development+Production envs -> first flag -> features page, with the new org/project coherent in the shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removes the FORCE_ON local-validation override so GettingStartedSwitch
gates on Utils.getFlagsmithHasFeature('onboarding_quickstart_flow').
Until the flag exists/is enabled on Flagsmith-on-Flagsmith, the check
returns false and the existing GettingStartedPage renders (safe default).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tracks entities already created (org, project, Dev/Prod envs, feature) in a ref so that retrying after a partial failure — e.g. project created but an environment call failed — reuses them instead of creating duplicates. On a fresh run the ref is empty and every step runs as before; only retries skip already-completed steps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The project step let Next stay enabled while the field was empty (it fell back to the placeholder), yet still rendered the empty field with the red invalid border — a contradiction. Require a name instead: Next is disabled until one is entered, so the red border correctly signals "needs input". The placeholder remains a hint, not a fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…loyment Per prototype feedback: only surface the invite CTA where it makes sense — self-hosted (any plan) or paid SaaS. On the free SaaS plan it's hidden rather than shown with an upgrade nudge, which would be friction at the success moment. When hidden, "Explore the dashboard" becomes the primary CTA and the supporting copy drops its invite reference (success panel and the PM intro paragraph). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng the flow The quickstart onboarding is a focused surface; the global Announcement / AnnouncementPerPage promo banners (e.g. event/workshop CTAs) are a distraction there. Suppress them while on /getting-started when the quickstart flow is enabled. Other pages and the legacy getting-started page are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The quickstart onboarding is a focused, isolated surface — render only the flow, bypassing the Nav shell (top bar, sidebar, project/account/docs links) so the customer can't navigate away mid-onboarding. The flow keeps its own explicit "Skip — set up manually" escape. Only applies on /getting-started when the quickstart flag is enabled; every other route renders the full shell as before. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thanks for submitting a PR! Please check the boxes below:
docs/if required so people know about the feature.Changes
Contributes to #7544
Working prototype of the role-branched onboarding quickstart flow, gated behind the
onboarding_quickstart_flowfeature flag (FORCE_ON = truefor local validation — flip toUtils.getFlagsmithHasFeature('onboarding_quickstart_flow')before merge).Flow shape
Keyboard navigation (per #7544 hard requirement)
role='radio'/aria-checkedsemantics:focus-visibleoutline restored (scoped) — the project-wide.btn:focus-visible { box-shadow: none }in_buttons.scsshides focus for sighted keyboard users; this PR scopes a restoration to.onboarding-quickstartwithout touching the global ruleOther notable changes
CodeSnippetrewritten with minimal per-language snippets, correct package names (@flagsmith/flagsmith), and the user's chosen feature name interpolatedBareButtonprimitive atweb/components/base/BareButton.tsxfor keyboard-accessible custom-styled surfaces (consumed in follow-up)text-mutedused in place oftext-secondarythroughout to avoid the Bootstrap\$secondary: #fae392collisionOut of scope (per issue)
handleFinishstill stubs the environment keyuseFirstEvaluationPollis placeholder polling; the real signal needs to pull from Influx (since flag evals go through the edge API now) and is tracked separatelyHow did you test this code?
GettingStartedSwitchworks (currentlyFORCE_ON = true)