Skip to content
Draft
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
2f5c6ee
feat(onboarding-quickstart): role-based onboarding with PM branching
talissoncosta May 28, 2026
3fc3cf3
feat(onboarding-quickstart): keyboard nav + PM integrations preview
talissoncosta May 28, 2026
20dd56b
feat(onboarding-quickstart): tidy Other role label and flow
talissoncosta May 28, 2026
2f17f3c
feat(onboarding-quickstart): address prototype feedback on project + …
talissoncosta May 29, 2026
664f39f
feat(onboarding-quickstart): add RTK mutations for org/project/enviro…
talissoncosta May 29, 2026
0db3388
feat(onboarding-quickstart): wire the create chain to real APIs
talissoncosta May 29, 2026
511af01
feat(onboarding-quickstart): route no-org signups into the flow + fix…
talissoncosta May 29, 2026
997204d
feat(onboarding-quickstart): gate the flow on the real feature flag
talissoncosta May 29, 2026
650433e
feat(onboarding-quickstart): make the create chain idempotent on retry
talissoncosta May 29, 2026
27a1c72
fix(onboarding-quickstart): require a project name before advancing
talissoncosta May 29, 2026
f5d8b73
feat(onboarding-quickstart): gate "Invite a teammate" by plan and dep…
talissoncosta May 29, 2026
5f51a6f
feat(onboarding-quickstart): hide marketing announcement banners duri…
talissoncosta May 29, 2026
a73bf8f
feat(onboarding-quickstart): render the flow chromeless (no app nav)
talissoncosta May 29, 2026
202d12b
chore(onboarding-quickstart): remove the temp "Test onboarding" nav link
talissoncosta Jun 1, 2026
cdcb7c8
feat(onboarding-quickstart): slim progress bar + concrete per-role copy
talissoncosta Jun 1, 2026
2a36424
feat(onboarding-quickstart): add shared FlagDemo (code + live app + t…
talissoncosta Jun 1, 2026
6312096
feat(onboarding-quickstart): add OnboardingSinglePage (AI + manual + …
talissoncosta Jun 3, 2026
09a598d
feat(onboarding-quickstart): wire single-page flow + multivariate var…
talissoncosta Jun 3, 2026
1b1f478
feat(onboarding-quickstart): real flag toggle, inline rename, idempot…
talissoncosta Jun 3, 2026
e407903
feat(onboarding-quickstart): concrete JS/React manual connect + theme…
talissoncosta Jun 3, 2026
4d9fd6e
chore(onboarding-quickstart): TEMP force single_page variant for the …
talissoncosta Jun 3, 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
12 changes: 12 additions & 0 deletions frontend/common/services/useEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ export const environmentService = service
.enhanceEndpoints({ addTagTypes: ['Environment'] })
.injectEndpoints({
endpoints: (builder) => ({
createEnvironment: builder.mutation<
Res['environment'],
Req['createEnvironment']
>({
invalidatesTags: [{ id: 'LIST', type: 'Environment' }],
query: (body: Req['createEnvironment']) => ({
body,
method: 'POST',
url: `environments/`,
}),
}),
getEnvironment: builder.query<Res['environment'], Req['getEnvironment']>({
providesTags: (res) => [{ id: res?.id, type: 'Environment' }],
query: (query: Req['getEnvironment']) => ({
Expand Down Expand Up @@ -84,6 +95,7 @@ export async function updateEnvironment(
// END OF FUNCTION_EXPORTS

export const {
useCreateEnvironmentMutation,
useGetEnvironmentMetricsQuery,
useGetEnvironmentQuery,
useGetEnvironmentsQuery,
Expand Down
12 changes: 12 additions & 0 deletions frontend/common/services/useOrganisation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ export const organisationService = service
.enhanceEndpoints({ addTagTypes: ['Organisation'] })
.injectEndpoints({
endpoints: (builder) => ({
createOrganisation: builder.mutation<
Res['organisation'],
Req['createOrganisation']
>({
invalidatesTags: [{ id: 'LIST', type: 'Organisation' }],
query: (body: Req['createOrganisation']) => ({
body,
method: 'POST',
url: `organisations/`,
}),
}),
deleteOrganisation: builder.mutation<void, Req['deleteOrganisation']>({
invalidatesTags: [{ id: 'LIST', type: 'Organisation' }],
query: ({ id }: Req['deleteOrganisation']) => ({
Expand Down Expand Up @@ -85,6 +96,7 @@ export async function getOrganisations(
// END OF FUNCTION_EXPORTS

export const {
useCreateOrganisationMutation,
useDeleteOrganisationMutation,
useGetOrganisationQuery,
useGetOrganisationsQuery,
Expand Down
9 changes: 9 additions & 0 deletions frontend/common/services/useProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ export const projectService = service
.enhanceEndpoints({ addTagTypes: ['Project'] })
.injectEndpoints({
endpoints: (builder) => ({
createProject: builder.mutation<Res['project'], Req['createProject']>({
invalidatesTags: [{ id: 'LIST', type: 'Project' }],
query: (body: Req['createProject']) => ({
body,
method: 'POST',
url: `projects/`,
}),
}),
deleteProject: builder.mutation<void, Req['deleteProject']>({
invalidatesTags: [{ id: 'LIST', type: 'Project' }],
query: ({ id }: Req['deleteProject']) => ({
Expand Down Expand Up @@ -99,6 +107,7 @@ export async function getProject(
// END OF FUNCTION_EXPORTS

export const {
useCreateProjectMutation,
useDeleteProjectMutation,
useGetProjectPermissionsQuery,
useGetProjectQuery,
Expand Down
3 changes: 3 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export type Req = {
}>
getOrganisations: {}
getOrganisation: { id: number }
createOrganisation: { name: string }
updateOrganisation: { id: number; body: UpdateOrganisationBody }
deleteOrganisation: { id: number }
uploadOrganisationLicence: {
Expand Down Expand Up @@ -635,6 +636,7 @@ export type Req = {
id: string
}
getProject: { id: number }
createProject: { name: string; organisation: number }
updateProject: { id: number; body: UpdateProjectBody }
deleteProject: { id: number }
migrateProject: { id: number }
Expand Down Expand Up @@ -682,6 +684,7 @@ export type Req = {
feature_id: number
group_ids: number[]
}
createEnvironment: { name: string; project: number }
updateEnvironment: { id: number; body: Environment }
createCloneIdentityFeatureStates: {
environment_id: string
Expand Down
65 changes: 45 additions & 20 deletions frontend/web/components/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,15 @@ const App = class extends Component {
}

if (!AccountStore.getOrganisation() && !invite) {
// If user has no organisation redirect to /create
this.props.history.replace(`/create${query}`)
// If user has no organisation redirect to /create — unless the new
// onboarding flow is enabled, which creates the organisation as its
// first step at /getting-started.
const noOrgDestination = Utils.getFlagsmithHasFeature(
'onboarding_quickstart_flow',
)
? '/getting-started'
: '/create'
this.props.history.replace(`${noOrgDestination}${query}`)
return
}

Expand Down Expand Up @@ -225,6 +232,12 @@ const App = class extends Component {
const projectId = this.getProjectId(this.props)
const environmentId = this.getEnvironmentId(this.props)

// The quickstart onboarding flow is a focused, distraction-free surface —
// suppress the marketing announcement banners while the user is in it.
const isOnboardingFlow =
pathname === '/getting-started' &&
Utils.getFlagsmithHasFeature('onboarding_quickstart_flow')

if (
AccountStore.getOrganisation() &&
AccountStore.getOrganisation().block_access_to_admin &&
Expand Down Expand Up @@ -273,24 +286,36 @@ const App = class extends Component {
onLogin={this.onLogin}
>
{({ isSaving, user }, { twoFactorLogin }) => {
return user && user.twoFactorPrompt ? (
<div className='col-md-6 push-md-3 mt-5'>
<TwoFactorPrompt
pin={this.state.pin}
error={this.state.error}
onSubmit={() => {
this.setState({ error: false })
twoFactorLogin(this.state.pin, () => {
this.setState({ error: true })
})
}}
isLoading={isSaving}
onChange={(e) =>
this.setState({ pin: Utils.safeParseEventValue(e) })
}
/>
</div>
) : (
if (user && user.twoFactorPrompt) {
return (
<div className='col-md-6 push-md-3 mt-5'>
<TwoFactorPrompt
pin={this.state.pin}
error={this.state.error}
onSubmit={() => {
this.setState({ error: false })
twoFactorLogin(this.state.pin, () => {
this.setState({ error: true })
})
}}
isLoading={isSaving}
onChange={(e) =>
this.setState({ pin: Utils.safeParseEventValue(e) })
}
/>
</div>
)
}

// Chromeless onboarding: render only the flow — no nav, sidebar,
// or header links — so the customer can't navigate away mid-flow.
// The flow provides its own explicit "Skip — set up manually"
// escape.
if (isOnboardingFlow) {
return <div>{this.props.children}</div>
}

return (
<Nav
header={
<>
Expand Down
26 changes: 26 additions & 0 deletions frontend/web/components/base/BareButton.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Reset for `<button>` primitives that need keyboard / screen-reader
// semantics but don't want the project's `.btn` chrome. Composed with
// utility classes for layout + appearance at the call site.
.bare-button {
appearance: none;
background: transparent;
border: 0;
padding: 0;
margin: 0;
font: inherit;
color: inherit;
text-align: inherit;
cursor: pointer;

&:disabled {
cursor: not-allowed;
opacity: 0.6;
}

// Accessible focus default. Consumers can override with their own
// focus-visible rules if they need a different indicator.
&:focus-visible {
outline: 2px solid var(--color-border-action);
outline-offset: 2px;
}
}
30 changes: 30 additions & 0 deletions frontend/web/components/base/BareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { ButtonHTMLAttributes, forwardRef } from 'react'
import './BareButton.scss'

type BareButtonProps = ButtonHTMLAttributes<HTMLButtonElement>

/**
* A button with no default chrome — just a clickable, keyboard-accessible
* surface. Use when you need a `<button>` for accessibility (keyboard
* activation, focus, screen-reader semantics) but the visual design is
* a styled custom shape — e.g. card rows, custom radios, icon-only
* triggers — that shouldn't inherit the project's `.btn` styling.
*
* Defaults `type='button'` so it never accidentally submits a parent
* form. Provides a `bare-button` reset class plus a default
* `:focus-visible` outline so keyboard users see where they are.
*/
const BareButton = forwardRef<HTMLButtonElement, BareButtonProps>(
({ className, type = 'button', ...rest }, ref) => (
<button
ref={ref}
type={type}
className={`bare-button ${className ?? ''}`.trim()}
{...rest}
/>
),
)

BareButton.displayName = 'BareButton'

export default BareButton
4 changes: 3 additions & 1 deletion frontend/web/components/base/forms/GhostInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ const GhostInput = forwardRef<HTMLInputElement, GhostInputProps>(

useEffect(() => {
if (spanRef.current) {
setInputWidth(spanRef.current.offsetWidth * 0.95)
// Match the measured text width plus a small buffer for the caret;
// anything less clips the last character(s).
setInputWidth(spanRef.current.offsetWidth + 2)
}
}, [value])

Expand Down
12 changes: 2 additions & 10 deletions frontend/web/components/navigation/navbars/TopNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Icon from 'components/icons/Icon'
import Headway from 'components/Headway'
import { Project } from 'common/types/responses'
import AccountDropdown from 'components/navigation/AccountDropdown'
import OnboardingChipWithDrawer from 'web/components/onboarding-quickstart/OnboardingChipWithDrawer'

type TopNavType = {
activeProject: Project | undefined
Expand All @@ -25,16 +26,7 @@ const TopNavbar: FC<TopNavType> = ({ activeProject, projectId }) => {
<div className='me-3'>
<GithubStar />
</div>
<NavLink
activeClassName='active'
to={'/getting-started'}
className='d-flex gap-1 d-none d-md-flex text-end lh-1 align-items-center'
>
<span>
<Icon name='rocket' width={20} fill='#9DA4AE' />
</span>
<span className='d-none d-md-block'>Getting Started</span>
</NavLink>
<OnboardingChipWithDrawer />
<a
className='d-flex gap-1 ps-3 text-end lh-1 align-items-center'
href={'https://docs.flagsmith.com'}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Slide-in concept-learning drawer. Single-mode; auto-dismissed by the
// chip when all items are complete. Tokens drive colour; this file owns
// the slide-in transform, backdrop fade, progress bar geometry, and the
// progress-fill width via a CSS custom property.

.concept-drawer {
position: fixed;
inset: 0 0 0 auto;
width: min(360px, 100vw);
z-index: 1050;
transform: translateX(100%);
transition: transform var(--duration-normal) var(--easing-standard);
border-left: 1px solid var(--color-border-default);
display: flex;
flex-direction: column;

&--open {
transform: translateX(0);
}

&__backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.12);
z-index: 1040;
opacity: 0;
pointer-events: none;
transition: opacity var(--duration-normal) var(--easing-standard);

&--open {
opacity: 1;
pointer-events: auto;
}
}

&__items {
overflow-y: auto;
flex: 1;
}

&__bar {
height: 6px;
overflow: hidden;
}

&__bar-fill {
height: 100%;
width: var(--concept-drawer-progress, 0%);
transition: width var(--duration-normal) var(--easing-standard);
}

&__marker {
display: inline-flex;
width: 22px;
height: 22px;
align-items: center;
justify-content: center;
}

&__marker-empty {
width: 14px;
height: 14px;
border-radius: 9999px;
border: 1.5px solid var(--color-border-strong);
}

&__item {
cursor: pointer;
transition: background-color var(--duration-fast) var(--easing-standard),
border-color var(--duration-fast) var(--easing-standard);

&:disabled {
cursor: default;
opacity: 0.7;
}
}
}

@media (prefers-reduced-motion: reduce) {
.concept-drawer,
.concept-drawer__backdrop,
.concept-drawer__bar-fill,
.concept-drawer__item {
transition: none;
}
}
Loading
Loading