From a137d58204308e8d7ed7ab74d4b6b43d0d20cb23 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 12 Aug 2026 17:18:08 -0300 Subject: [PATCH 01/15] feat(onboarding): add the gradual rollout quest screen The "Gradual rollout" card deep-linked straight to the segment overrides tab, which explains none of the steps a rollout takes. It now opens a quest screen first: the three manual steps from the rollout guide, the identify prerequisite, and a coming-soon block gauging demand for doing it in one action. The open quest lives in the URL, so refresh and back behave, and "Create a rollout segment" continues to where the card used to go. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/common/constants.ts | 17 ++++ .../OnboardingRolloutQuest.stories.tsx | 34 +++++++ .../OnboardingFlow/OnboardingFlow.tsx | 53 ++++++++++- .../OnboardingRolloutQuest.scss | 58 ++++++++++++ .../OnboardingRolloutQuest.tsx | 92 +++++++++++++++++++ .../RolloutComingSoonCard.tsx | 64 +++++++++++++ .../OnboardingRolloutQuest/index.ts | 2 + .../OnboardingRolloutQuest/rolloutSteps.tsx | 37 ++++++++ 8 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 frontend/documentation/pages/onboarding/OnboardingRolloutQuest.stories.tsx create mode 100644 frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss create mode 100644 frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx create mode 100644 frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx create mode 100644 frontend/web/components/pages/onboarding/OnboardingRolloutQuest/index.ts create mode 100644 frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx diff --git a/frontend/common/constants.ts b/frontend/common/constants.ts index f11d5a27f9cb..8a46b1ffc552 100644 --- a/frontend/common/constants.ts +++ b/frontend/common/constants.ts @@ -284,6 +284,23 @@ const Constants = { 'category': 'Onboarding', 'event': 'Onboarding flag toggled', }, + 'ONBOARDING_ROLLOUT_CONTINUED': { + 'category': 'Onboarding', + 'event': 'Onboarding rollout quest continued', + }, + 'ONBOARDING_ROLLOUT_FEEDBACK': { + 'category': 'Onboarding', + 'event': 'Onboarding rollout feedback opened', + }, + // The fake door's only real signal: demand for one-click rollouts. + 'ONBOARDING_ROLLOUT_NOTIFY_ME': { + 'category': 'Onboarding', + 'event': 'Onboarding rollout notify me', + }, + 'ONBOARDING_ROLLOUT_VIEWED': { + 'category': 'Onboarding', + 'event': 'Onboarding rollout quest viewed', + }, 'ONBOARDING_SNIPPET_COPIED': { 'category': 'Onboarding', 'event': 'Onboarding snippet copied', diff --git a/frontend/documentation/pages/onboarding/OnboardingRolloutQuest.stories.tsx b/frontend/documentation/pages/onboarding/OnboardingRolloutQuest.stories.tsx new file mode 100644 index 000000000000..6cf22c111d54 --- /dev/null +++ b/frontend/documentation/pages/onboarding/OnboardingRolloutQuest.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from 'storybook' + +import OnboardingRolloutQuest from 'components/pages/onboarding/OnboardingRolloutQuest' + +const meta: Meta = { + args: { + featureName: 'checkout_v2', + onContinue: () => {}, + onDismiss: () => {}, + onFeedback: () => {}, + onNotifyMe: () => {}, + }, + component: OnboardingRolloutQuest, + parameters: { + docs: { + description: { + component: + 'The "Gradual rollout" quest, shown between the next-step card and the flag\'s segment overrides tab. Explains the three manual steps a rollout takes today, flags the identify prerequisite, and gauges demand for doing it in one action.', + }, + }, + layout: 'padded', + }, + title: 'Pages/Onboarding/OnboardingRolloutQuest', +} +export default meta + +type Story = StoryObj + +export const Default: Story = {} + +// The flag name threads through the subtitle and step 2. +export const LongFeatureName: Story = { + args: { featureName: 'enable_new_checkout_experience_for_mobile' }, +} diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index c69406851763..644406872ee1 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -1,5 +1,5 @@ import React, { FC, useEffect, useState } from 'react' -import { useHistory } from 'react-router-dom' +import { useHistory, useLocation } from 'react-router-dom' import Button from 'components/base/forms/Button' import Icon from 'components/icons/Icon' import OnboardingHeader from 'components/pages/onboarding/OnboardingHeader' @@ -10,6 +10,7 @@ import OnboardingFlagsTable from 'components/pages/onboarding/OnboardingFlagsTab import OnboardingNextSteps, { OnboardingNextStep, } from 'components/pages/onboarding/OnboardingNextSteps' +import OnboardingRolloutQuest from 'components/pages/onboarding/OnboardingRolloutQuest' import { useEnsureOnboardingResources } from 'components/pages/onboarding/hooks/useEnsureOnboardingResources' import { useOnboardingFlagRename } from 'components/pages/onboarding/hooks/useOnboardingFlagRename' import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardingFlag' @@ -39,6 +40,10 @@ const OnboardingFlow: FC = () => { } = useEnsureOnboardingResources() const history = useHistory() + const location = useLocation() + // The open quest lives in the URL, so refresh and back both behave. + const rolloutQuestOpen = + new URLSearchParams(location.search).get('quest') === 'rollout' const [updateOrganisation] = useUpdateOrganisationMutation() const [updateProject] = useUpdateProjectMutation() @@ -144,8 +149,8 @@ const OnboardingFlow: FC = () => { } } - // Each next-step card deep-links to the flag's real config; nothing faked. - const goToNextStep = (step: OnboardingNextStep) => { + // Where a quest ends up: the flag's real config, nothing faked. + const goToFlagConfig = (step: OnboardingNextStep) => { if (projectId === null) { return } @@ -162,6 +167,17 @@ const OnboardingFlow: FC = () => { history.push(`${base}/features?feature=${flagId}&tab=${tab}`) } + // Rollout is the one quest with a screen of its own: the segment overrides + // tab alone explains none of the steps a rollout takes. The others still go + // straight to their config. The param keeps the screen on refresh and back. + const goToNextStep = (step: OnboardingNextStep) => { + if (step === 'rollout') { + history.push('/getting-started?quest=rollout') + return + } + goToFlagConfig(step) + } + const diagnosticIds = { environment_id: environment?.id, organisation_id: organisationId, @@ -193,6 +209,18 @@ const OnboardingFlow: FC = () => { }) } } + const trackRollout = (event: { category: string; event: string }) => + API.trackEvent({ ...event, extra: diagnosticIds }) + + useEffect(() => { + if (!rolloutQuestOpen) return + API.trackEvent({ + ...Constants.events.ONBOARDING_ROLLOUT_VIEWED, + extra: diagnosticIds, + }) + // Once per opening, not on every id settling. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rolloutQuestOpen]) if (status === 'creating' || leavingForConsent) { return ( @@ -215,6 +243,25 @@ const OnboardingFlow: FC = () => { ) } + if (rolloutQuestOpen) { + return ( + { + trackRollout(Constants.events.ONBOARDING_ROLLOUT_CONTINUED) + goToFlagConfig('rollout') + }} + onDismiss={() => history.push('/getting-started')} + onNotifyMe={() => + trackRollout(Constants.events.ONBOARDING_ROLLOUT_NOTIFY_ME) + } + onFeedback={() => + trackRollout(Constants.events.ONBOARDING_ROLLOUT_FEEDBACK) + } + /> + ) + } + return (
diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss new file mode 100644 index 000000000000..a26e5da8df96 --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss @@ -0,0 +1,58 @@ +// The gradual rollout quest. Layout comes from utilities in the markup; this +// file holds type sizes, the two card paddings, the numbered step markers and +// the "Coming soon" badge. Secondary text isn't a utility: Bootstrap's +// .text-secondary (gold, !important) would win over our token utility. +.onboarding-rollout-quest { + max-width: 720px; + + &__title { + font-size: 24px; + } + + &__card-title, + &__soon-title { + font-size: 16px; + } + + &__body { + font-size: 13px; + line-height: 1.5; + color: var(--color-text-secondary); + } + + &__card, + &__soon { + padding: 24px; + } + + // Numbered, but the marker is the styled circle below, not a list bullet. + &__steps { + list-style: none; + } + + &__step-number { + width: 24px; + height: 24px; + font-size: 12px; + font-weight: 700; + } + + &__step-title { + font-size: 14px; + } + + // The icon inherits it via currentColor. + &__notified { + font-size: 13px; + } + + &__prerequisite { + font-size: 12px; + line-height: 1.5; + color: var(--color-text-secondary); + + code { + font-size: 12px; + } + } +} diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx new file mode 100644 index 000000000000..38b6f711d7fe --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -0,0 +1,92 @@ +import React, { FC } from 'react' +import Button from 'components/base/forms/Button' +import Icon from 'components/icons/Icon' +import RolloutComingSoonCard from './RolloutComingSoonCard' +import { + ROLLOUT_GUIDE_URL, + RolloutPrerequisite, + getRolloutSteps, +} from './rolloutSteps' +import './OnboardingRolloutQuest.scss' + +export type OnboardingRolloutQuestProps = { + featureName: string + // Continues to the flag's segment overrides, where the rollout is set up. + onContinue: () => void + onDismiss: () => void + onNotifyMe: () => void + onFeedback: () => void +} + +// The "Gradual rollout" quest. It sits between the next-step card and the +// segment overrides tab, because landing straight in that tab explains none of +// the steps the rollout actually takes. +const OnboardingRolloutQuest: FC = ({ + featureName, + onContinue, + onDismiss, + onFeedback, + onNotifyMe, +}) => ( +
+
+

+ Roll out gradually +

+

+ Release {featureName} to a growing percentage of your users. +

+
+ +
+

+ How to roll out gradually today +

+
    + {getRolloutSteps(featureName).map((step, index) => ( +
  1. + + {index + 1} + + + + {step.title} + + + {step.body} + + +
  2. + ))} +
+ + +
+ + + +
+ + +
+
+) + +export default OnboardingRolloutQuest diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx new file mode 100644 index 000000000000..5c9a815efe63 --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx @@ -0,0 +1,64 @@ +import React, { FC, useState } from 'react' +import Button from 'components/base/forms/Button' +import Chip from 'components/base/Chip' +import Icon from 'components/icons/Icon' + +export const ROLLOUT_FEEDBACK_URL = + 'mailto:support@flagsmith.com?subject=Gradual%20rollout' + +export type RolloutComingSoonCardProps = { + onNotifyMe: () => void + onFeedback: () => void +} + +// The one thing we don't ship yet: doing the three steps above in a single +// action. It promises a simpler flow, never the capability, which already +// exists. +const RolloutComingSoonCard: FC = ({ + onFeedback, + onNotifyMe, +}) => { + const [notified, setNotified] = useState(false) + const notifyMe = () => { + setNotified(true) + onNotifyMe() + } + return ( +
+ {/* Accent rather than the design's solid purple: there is no inverse + text token, and white on the dark-mode action surface is ~3.2:1. */} + + + Coming soon + +

+ We’re making gradual rollouts one-click +

+

+ Soon you’ll ramp a flag up on a schedule automatically, without editing + segments by hand. Want early access? +

+
+ {notified ? ( + + + Thanks, we’ll be in touch. + + ) : ( + + )} + {/* No target: a mailto in a new tab leaves a blank tab behind. */} + +
+
+ ) +} + +export default RolloutComingSoonCard diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/index.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/index.ts new file mode 100644 index 000000000000..e8c85c3f8916 --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/index.ts @@ -0,0 +1,2 @@ +export { default } from './OnboardingRolloutQuest' +export type { OnboardingRolloutQuestProps } from './OnboardingRolloutQuest' diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx new file mode 100644 index 000000000000..7e352ff377c0 --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx @@ -0,0 +1,37 @@ +import React, { FC } from 'react' +import Icon from 'components/icons/Icon' + +export const ROLLOUT_GUIDE_URL = + 'https://docs.flagsmith.com/managing-flags/rollout/rollout-by-percentage' + +// The manual method, as the guide describes it. Written as the steps someone +// takes today, not as a workaround: gradual rollout is possible right now. +export const getRolloutSteps = ( + featureName: string, +): { title: string; body: string }[] => [ + { + body: 'Add a “Percentage split” rule and set your starting share, e.g. 10% of users.', + title: 'Create a segment', + }, + { + body: `Turn ${featureName} on for that segment, so only those users get it.`, + title: 'Override the flag', + }, + { + body: 'Raise the percentage as your confidence grows: 10% → 25% → 50% → 100%.', + title: 'Increase over time', + }, +] + +// The prerequisite the guide flags. Without it the override reaches nobody and +// the user has no way to tell, so it sits with the steps rather than in docs. +export const RolloutPrerequisite: FC = () => ( +

+ + + Percentage splits only apply to users you identify. If your app doesn’t + call flagsmith.identify(...) yet, add it first, or everyone + keeps getting the same value. + +

+) From 3e462e1e890fcffb9ecc91701ee43674063455be Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 11:15:15 -0300 Subject: [PATCH 02/15] feat(onboarding): make the rollout door's interest actionable Notify me and the feedback link fired onboarding funnel events carrying environment, organisation and project ids. That records that some organisation wants this and leaves nobody to reply to, so "we'll be in touch" was a promise we could not keep. They now also report where the other fake doors report, with the email and organisation name, matching the segment sources door in #8272. The funnel events stay as they were, since they are steps rather than signal. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingFlow/OnboardingFlow.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index 644406872ee1..06e083891fee 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -20,6 +20,8 @@ import { useUpdateProjectMutation } from 'common/services/useProject' import API from 'project/api' import Constants from 'common/constants' import { isPendingAuthorisation } from 'common/utils/pendingAuthorisation' +import flagsmith from '@flagsmith/flagsmith' +import AccountStore from 'common/stores/account-store' import './OnboardingFlow.scss' type OnboardingSnippet = 'install' | 'wire' @@ -212,6 +214,18 @@ const OnboardingFlow: FC = () => { const trackRollout = (event: { category: string; event: string }) => API.trackEvent({ ...event, extra: diagnosticIds }) + // Interest in the door goes where the other fake doors report, carrying who + // asked. Ids alone tell us an organisation wants this and give us no way to + // reply to them. Matches the segment sources door in #8272. + const trackRolloutInterest = (event: string) => + flagsmith.trackEvent(event, { + metadata: { + email: AccountStore.getUser()?.email, + organisation: AccountStore.getOrganisation()?.name, + source: 'onboarding-rollout-quest', + }, + }) + useEffect(() => { if (!rolloutQuestOpen) return API.trackEvent({ @@ -252,12 +266,14 @@ const OnboardingFlow: FC = () => { goToFlagConfig('rollout') }} onDismiss={() => history.push('/getting-started')} - onNotifyMe={() => + onNotifyMe={() => { trackRollout(Constants.events.ONBOARDING_ROLLOUT_NOTIFY_ME) - } - onFeedback={() => + trackRolloutInterest('rollout_beta_requested') + }} + onFeedback={() => { trackRollout(Constants.events.ONBOARDING_ROLLOUT_FEEDBACK) - } + trackRolloutInterest('rollout_feedback_clicked') + }} /> ) } From 73e490e6d6674c62a456e08733a36447b2f487c4 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 11:19:06 -0300 Subject: [PATCH 03/15] refactor(onboarding): move the rollout door tracking out of the flow OnboardingFlow is long enough without two more imports for four lines of analytics, so the interest tracker is its own module beside the quest it belongs to, with the event names alongside it. It also takes who is asking rather than reading it. That drops AccountStore in favour of the profile query the onboarding hook already runs, so this is a cache read, and the organisation is the renamed one the user is looking at rather than whatever the Flux store last held. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingFlow/OnboardingFlow.tsx | 29 ++++++++-------- .../trackRolloutInterest.ts | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index 06e083891fee..5e8ac5faa920 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -11,17 +11,20 @@ import OnboardingNextSteps, { OnboardingNextStep, } from 'components/pages/onboarding/OnboardingNextSteps' import OnboardingRolloutQuest from 'components/pages/onboarding/OnboardingRolloutQuest' +import trackRolloutInterest, { + ROLLOUT_BETA_REQUESTED, + ROLLOUT_FEEDBACK_CLICKED, +} from 'components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest' import { useEnsureOnboardingResources } from 'components/pages/onboarding/hooks/useEnsureOnboardingResources' import { useOnboardingFlagRename } from 'components/pages/onboarding/hooks/useOnboardingFlagRename' import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardingFlag' import { useOnboardingConnection } from 'components/pages/onboarding/hooks/useOnboardingConnection' import { useUpdateOrganisationMutation } from 'common/services/useOrganisation' import { useUpdateProjectMutation } from 'common/services/useProject' +import { useGetProfileQuery } from 'common/services/useProfile' import API from 'project/api' import Constants from 'common/constants' import { isPendingAuthorisation } from 'common/utils/pendingAuthorisation' -import flagsmith from '@flagsmith/flagsmith' -import AccountStore from 'common/stores/account-store' import './OnboardingFlow.scss' type OnboardingSnippet = 'install' | 'wire' @@ -48,6 +51,8 @@ const OnboardingFlow: FC = () => { new URLSearchParams(location.search).get('quest') === 'rollout' const [updateOrganisation] = useUpdateOrganisationMutation() const [updateProject] = useUpdateProjectMutation() + // Already fetched by useEnsureOnboardingResources, so this is a cache read. + const { data: profile } = useGetProfileQuery({}) // A client waiting on the consent screen sent this user here to sign up. It // is answered once the workspace exists, never on load: bootstrapping is a @@ -213,18 +218,10 @@ const OnboardingFlow: FC = () => { } const trackRollout = (event: { category: string; event: string }) => API.trackEvent({ ...event, extra: diagnosticIds }) - - // Interest in the door goes where the other fake doors report, carrying who - // asked. Ids alone tell us an organisation wants this and give us no way to - // reply to them. Matches the segment sources door in #8272. - const trackRolloutInterest = (event: string) => - flagsmith.trackEvent(event, { - metadata: { - email: AccountStore.getUser()?.email, - organisation: AccountStore.getOrganisation()?.name, - source: 'onboarding-rollout-quest', - }, - }) + const whoIsAsking = { + email: profile?.email, + organisation: organisationDisplayName, + } useEffect(() => { if (!rolloutQuestOpen) return @@ -268,11 +265,11 @@ const OnboardingFlow: FC = () => { onDismiss={() => history.push('/getting-started')} onNotifyMe={() => { trackRollout(Constants.events.ONBOARDING_ROLLOUT_NOTIFY_ME) - trackRolloutInterest('rollout_beta_requested') + trackRolloutInterest(ROLLOUT_BETA_REQUESTED, whoIsAsking) }} onFeedback={() => { trackRollout(Constants.events.ONBOARDING_ROLLOUT_FEEDBACK) - trackRolloutInterest('rollout_feedback_clicked') + trackRolloutInterest(ROLLOUT_FEEDBACK_CLICKED, whoIsAsking) }} /> ) diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts new file mode 100644 index 000000000000..28734a7d6ac6 --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts @@ -0,0 +1,33 @@ +import flagsmith from '@flagsmith/flagsmith' + +export const ROLLOUT_BETA_REQUESTED = 'rollout_beta_requested' +export const ROLLOUT_FEEDBACK_CLICKED = 'rollout_feedback_clicked' + +type RolloutInterest = { + email?: string + organisation?: string +} + +/** + * Interest in the door, reported where the other fake doors report and + * carrying who asked. The onboarding funnel events record that an + * organisation wants this; these record who to reply to. Same shape as the + * segment sources door. + * + * Takes who rather than reading it, so the caller supplies it from the queries + * it already holds. + */ +const trackRolloutInterest = ( + event: string, + { email, organisation }: RolloutInterest, +): void => { + flagsmith.trackEvent(event, { + metadata: { + email, + organisation, + source: 'onboarding-rollout-quest', + }, + }) +} + +export default trackRolloutInterest From 924fdb8df73d9d2045b8420389df57b2ab697914 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 11:24:28 -0300 Subject: [PATCH 04/15] chore(onboarding): drop the rollout quest story A one-off onboarding screen rather than a component anyone reuses, so there is nothing for a story to document that the screen itself does not. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingRolloutQuest.stories.tsx | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 frontend/documentation/pages/onboarding/OnboardingRolloutQuest.stories.tsx diff --git a/frontend/documentation/pages/onboarding/OnboardingRolloutQuest.stories.tsx b/frontend/documentation/pages/onboarding/OnboardingRolloutQuest.stories.tsx deleted file mode 100644 index 6cf22c111d54..000000000000 --- a/frontend/documentation/pages/onboarding/OnboardingRolloutQuest.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import type { Meta, StoryObj } from 'storybook' - -import OnboardingRolloutQuest from 'components/pages/onboarding/OnboardingRolloutQuest' - -const meta: Meta = { - args: { - featureName: 'checkout_v2', - onContinue: () => {}, - onDismiss: () => {}, - onFeedback: () => {}, - onNotifyMe: () => {}, - }, - component: OnboardingRolloutQuest, - parameters: { - docs: { - description: { - component: - 'The "Gradual rollout" quest, shown between the next-step card and the flag\'s segment overrides tab. Explains the three manual steps a rollout takes today, flags the identify prerequisite, and gauges demand for doing it in one action.', - }, - }, - layout: 'padded', - }, - title: 'Pages/Onboarding/OnboardingRolloutQuest', -} -export default meta - -type Story = StoryObj - -export const Default: Story = {} - -// The flag name threads through the subtitle and step 2. -export const LongFeatureName: Story = { - args: { featureName: 'enable_new_checkout_experience_for_mobile' }, -} From b353bf380729257eb041b635eac79b35c39a3257 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 11:30:16 -0300 Subject: [PATCH 05/15] refactor(onboarding): give the rollout quest its own hook The quest had taken 64 of OnboardingFlow's 318 lines: a URL param, four callbacks, a view effect and two tracking helpers, for one of five next steps. useRolloutQuest owns all of it and the flow asks whether it is open. OnboardingFlow drops to 277 lines and its share of the quest to eight: the hook call, a ternary in goToNextStep, and the early return. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingFlow/OnboardingFlow.tsx | 71 ++++------------- .../OnboardingRolloutQuest/index.ts | 1 + .../OnboardingRolloutQuest/useRolloutQuest.ts | 79 +++++++++++++++++++ 3 files changed, 95 insertions(+), 56 deletions(-) create mode 100644 frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index 5e8ac5faa920..83a2ac8fe7be 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -1,5 +1,5 @@ -import React, { FC, useEffect, useState } from 'react' -import { useHistory, useLocation } from 'react-router-dom' +import React, { FC, useState } from 'react' +import { useHistory } from 'react-router-dom' import Button from 'components/base/forms/Button' import Icon from 'components/icons/Icon' import OnboardingHeader from 'components/pages/onboarding/OnboardingHeader' @@ -10,11 +10,9 @@ import OnboardingFlagsTable from 'components/pages/onboarding/OnboardingFlagsTab import OnboardingNextSteps, { OnboardingNextStep, } from 'components/pages/onboarding/OnboardingNextSteps' -import OnboardingRolloutQuest from 'components/pages/onboarding/OnboardingRolloutQuest' -import trackRolloutInterest, { - ROLLOUT_BETA_REQUESTED, - ROLLOUT_FEEDBACK_CLICKED, -} from 'components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest' +import OnboardingRolloutQuest, { + useRolloutQuest, +} from 'components/pages/onboarding/OnboardingRolloutQuest' import { useEnsureOnboardingResources } from 'components/pages/onboarding/hooks/useEnsureOnboardingResources' import { useOnboardingFlagRename } from 'components/pages/onboarding/hooks/useOnboardingFlagRename' import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardingFlag' @@ -45,10 +43,6 @@ const OnboardingFlow: FC = () => { } = useEnsureOnboardingResources() const history = useHistory() - const location = useLocation() - // The open quest lives in the URL, so refresh and back both behave. - const rolloutQuestOpen = - new URLSearchParams(location.search).get('quest') === 'rollout' const [updateOrganisation] = useUpdateOrganisationMutation() const [updateProject] = useUpdateProjectMutation() // Already fetched by useEnsureOnboardingResources, so this is a cache read. @@ -174,22 +168,20 @@ const OnboardingFlow: FC = () => { history.push(`${base}/features?feature=${flagId}&tab=${tab}`) } - // Rollout is the one quest with a screen of its own: the segment overrides - // tab alone explains none of the steps a rollout takes. The others still go - // straight to their config. The param keeps the screen on refresh and back. - const goToNextStep = (step: OnboardingNextStep) => { - if (step === 'rollout') { - history.push('/getting-started?quest=rollout') - return - } - goToFlagConfig(step) - } + const goToNextStep = (step: OnboardingNextStep) => + step === 'rollout' ? rolloutQuest.open() : goToFlagConfig(step) const diagnosticIds = { environment_id: environment?.id, organisation_id: organisationId, project_id: projectId, } + const rolloutQuest = useRolloutQuest({ + diagnosticIds, + featureName, + onContinue: () => goToFlagConfig('rollout'), + who: { email: profile?.email, organisation: organisationDisplayName }, + }) const trackSnippetCopied = (snippet: OnboardingSnippet) => API.trackEvent({ ...Constants.events.ONBOARDING_SNIPPET_COPIED, @@ -216,22 +208,6 @@ const OnboardingFlow: FC = () => { }) } } - const trackRollout = (event: { category: string; event: string }) => - API.trackEvent({ ...event, extra: diagnosticIds }) - const whoIsAsking = { - email: profile?.email, - organisation: organisationDisplayName, - } - - useEffect(() => { - if (!rolloutQuestOpen) return - API.trackEvent({ - ...Constants.events.ONBOARDING_ROLLOUT_VIEWED, - extra: diagnosticIds, - }) - // Once per opening, not on every id settling. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rolloutQuestOpen]) if (status === 'creating' || leavingForConsent) { return ( @@ -254,25 +230,8 @@ const OnboardingFlow: FC = () => { ) } - if (rolloutQuestOpen) { - return ( - { - trackRollout(Constants.events.ONBOARDING_ROLLOUT_CONTINUED) - goToFlagConfig('rollout') - }} - onDismiss={() => history.push('/getting-started')} - onNotifyMe={() => { - trackRollout(Constants.events.ONBOARDING_ROLLOUT_NOTIFY_ME) - trackRolloutInterest(ROLLOUT_BETA_REQUESTED, whoIsAsking) - }} - onFeedback={() => { - trackRollout(Constants.events.ONBOARDING_ROLLOUT_FEEDBACK) - trackRolloutInterest(ROLLOUT_FEEDBACK_CLICKED, whoIsAsking) - }} - /> - ) + if (rolloutQuest.isOpen) { + return } return ( diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/index.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/index.ts index e8c85c3f8916..90fc8dc7d31e 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/index.ts +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/index.ts @@ -1,2 +1,3 @@ export { default } from './OnboardingRolloutQuest' export type { OnboardingRolloutQuestProps } from './OnboardingRolloutQuest' +export { default as useRolloutQuest } from './useRolloutQuest' diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts new file mode 100644 index 000000000000..1fd0fa401a2a --- /dev/null +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts @@ -0,0 +1,79 @@ +import { useEffect } from 'react' +import { useHistory, useLocation } from 'react-router-dom' +import API from 'project/api' +import Constants from 'common/constants' +import trackRolloutInterest, { + ROLLOUT_BETA_REQUESTED, + ROLLOUT_FEEDBACK_CLICKED, +} from './trackRolloutInterest' +import { OnboardingRolloutQuestProps } from './OnboardingRolloutQuest' + +type UseRolloutQuest = { + featureName: string + /** Where "Create a rollout segment" ends up. */ + onContinue: () => void + /** Onboarding funnel context, as the other onboarding events send it. */ + diagnosticIds: Record + /** Who to reply to when someone asks for access. */ + who: { email?: string; organisation?: string } +} + +/** + * The rollout quest's own routing and analytics, so the flow only has to ask + * whether it is open. + * + * Rollout is the one quest with a screen of its own: the segment overrides tab + * alone explains none of the steps a rollout takes. The rest still go straight + * to their config. The open quest lives in the URL, so refresh and back both + * behave. + */ +const useRolloutQuest = ({ + diagnosticIds, + featureName, + onContinue, + who, +}: UseRolloutQuest): { + isOpen: boolean + open: () => void + props: OnboardingRolloutQuestProps +} => { + const history = useHistory() + const location = useLocation() + const isOpen = new URLSearchParams(location.search).get('quest') === 'rollout' + + const track = (event: { category: string; event: string }) => + API.trackEvent({ ...event, extra: diagnosticIds }) + + useEffect(() => { + if (!isOpen) return + API.trackEvent({ + ...Constants.events.ONBOARDING_ROLLOUT_VIEWED, + extra: diagnosticIds, + }) + // Once per opening, not on every id settling. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]) + + return { + isOpen, + open: () => history.push('/getting-started?quest=rollout'), + props: { + featureName, + onContinue: () => { + track(Constants.events.ONBOARDING_ROLLOUT_CONTINUED) + onContinue() + }, + onDismiss: () => history.push('/getting-started'), + onFeedback: () => { + track(Constants.events.ONBOARDING_ROLLOUT_FEEDBACK) + trackRolloutInterest(ROLLOUT_FEEDBACK_CLICKED, who) + }, + onNotifyMe: () => { + track(Constants.events.ONBOARDING_ROLLOUT_NOTIFY_ME) + trackRolloutInterest(ROLLOUT_BETA_REQUESTED, who) + }, + }, + } +} + +export default useRolloutQuest From af5afaee3246324977892681d0d996ab3f9bca9c Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 11:33:52 -0300 Subject: [PATCH 06/15] chore(onboarding): trim the rollout quest comments The stylesheet header inventoried the file's contents, which goes stale on the first new rule. Only the Bootstrap collision survives, since that is the part you cannot work out by reading it. Two explanations appeared twice: why the quest gets a screen rather than a deep link, which now lives only in useRolloutQuest where the routing decision is, and that this promises the simplification rather than the capability, which now lives only on the card making the promise. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingRolloutQuest/OnboardingRolloutQuest.scss | 6 ++---- .../OnboardingRolloutQuest/OnboardingRolloutQuest.tsx | 4 ---- .../onboarding/OnboardingRolloutQuest/rolloutSteps.tsx | 3 +-- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss index a26e5da8df96..b5b9b57c0ee9 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss @@ -1,7 +1,5 @@ -// The gradual rollout quest. Layout comes from utilities in the markup; this -// file holds type sizes, the two card paddings, the numbered step markers and -// the "Coming soon" badge. Secondary text isn't a utility: Bootstrap's -// .text-secondary (gold, !important) would win over our token utility. +// Secondary text isn't a utility here: Bootstrap's .text-secondary is gold and +// !important, so it would win over our token utility. .onboarding-rollout-quest { max-width: 720px; diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx index 38b6f711d7fe..2ede12bbd4e1 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -11,16 +11,12 @@ import './OnboardingRolloutQuest.scss' export type OnboardingRolloutQuestProps = { featureName: string - // Continues to the flag's segment overrides, where the rollout is set up. onContinue: () => void onDismiss: () => void onNotifyMe: () => void onFeedback: () => void } -// The "Gradual rollout" quest. It sits between the next-step card and the -// segment overrides tab, because landing straight in that tab explains none of -// the steps the rollout actually takes. const OnboardingRolloutQuest: FC = ({ featureName, onContinue, diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx index 7e352ff377c0..4f0125907b7c 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx @@ -4,8 +4,7 @@ import Icon from 'components/icons/Icon' export const ROLLOUT_GUIDE_URL = 'https://docs.flagsmith.com/managing-flags/rollout/rollout-by-percentage' -// The manual method, as the guide describes it. Written as the steps someone -// takes today, not as a workaround: gradual rollout is possible right now. +// The steps someone takes today, not a workaround: this works right now. export const getRolloutSteps = ( featureName: string, ): { title: string; body: string }[] => [ From c4c27075ebb1258e3ba47bbb99273951c79caa0b Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 11:37:55 -0300 Subject: [PATCH 07/15] refactor(onboarding): use the text-secondary token utility The stylesheet set the colour itself, with a comment claiming Bootstrap's .text-secondary would win the cascade. It would not: _bootstrap.scss drops secondary from Bootstrap's colour utility map for exactly that reason, so .text-secondary already resolves to the token. Two colour declarations gone. The line-height stays custom and now says why: .lh-base is 1.375 here, not 1.5. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingRolloutQuest/OnboardingRolloutQuest.scss | 5 +---- .../OnboardingRolloutQuest/OnboardingRolloutQuest.tsx | 4 ++-- .../OnboardingRolloutQuest/RolloutComingSoonCard.tsx | 2 +- .../pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss index b5b9b57c0ee9..f1f60640977c 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss @@ -1,5 +1,3 @@ -// Secondary text isn't a utility here: Bootstrap's .text-secondary is gold and -// !important, so it would win over our token utility. .onboarding-rollout-quest { max-width: 720px; @@ -12,10 +10,10 @@ font-size: 16px; } + // 1.5 rather than .lh-base, which is 1.375 here. &__body { font-size: 13px; line-height: 1.5; - color: var(--color-text-secondary); } &__card, @@ -47,7 +45,6 @@ &__prerequisite { font-size: 12px; line-height: 1.5; - color: var(--color-text-secondary); code { font-size: 12px; diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx index 2ede12bbd4e1..87f6f4224646 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -29,7 +29,7 @@ const OnboardingRolloutQuest: FC = ({

Roll out gradually

-

+

Release {featureName} to a growing percentage of your users.

@@ -48,7 +48,7 @@ const OnboardingRolloutQuest: FC = ({ {step.title} - + {step.body} diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx index 5c9a815efe63..3227bfb21dcd 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx @@ -34,7 +34,7 @@ const RolloutComingSoonCard: FC = ({

We’re making gradual rollouts one-click

-

+

Soon you’ll ramp a flag up on a schedule automatically, without editing segments by hand. Want early access?

diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx index 4f0125907b7c..910bc9942b97 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx @@ -25,7 +25,7 @@ export const getRolloutSteps = ( // The prerequisite the guide flags. Without it the override reaches nobody and // the user has no way to tell, so it sits with the steps rather than in docs. export const RolloutPrerequisite: FC = () => ( -

+

Percentage splits only apply to users you identify. If your app doesn’t From 01dcb9e41d303c99bfa2d909250b87649b863e02 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 12:11:11 -0300 Subject: [PATCH 08/15] refactor(onboarding): open the rollout quest as a modal Matches the segment sources fake door, which is the pattern the codebase now has for these. The quest was the only next step that took over the page, which made one of five cards behave unlike the rest. The URL param goes with it, so refresh no longer keeps the quest open and browser back leaves onboarding rather than closing it. Accepted: the modal is dismissable and the card reopens it. The modal chrome supplies the title and the close control, so the screen's own heading goes, and the hook is down to opening it with its analytics. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingFlow/OnboardingFlow.tsx | 16 ++-- .../OnboardingRolloutQuest.scss | 6 -- .../OnboardingRolloutQuest.tsx | 13 +--- .../OnboardingRolloutQuest/useRolloutQuest.ts | 76 +++++++------------ 4 files changed, 38 insertions(+), 73 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index 83a2ac8fe7be..ade40f851657 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -10,9 +10,7 @@ import OnboardingFlagsTable from 'components/pages/onboarding/OnboardingFlagsTab import OnboardingNextSteps, { OnboardingNextStep, } from 'components/pages/onboarding/OnboardingNextSteps' -import OnboardingRolloutQuest, { - useRolloutQuest, -} from 'components/pages/onboarding/OnboardingRolloutQuest' +import { useRolloutQuest } from 'components/pages/onboarding/OnboardingRolloutQuest' import { useEnsureOnboardingResources } from 'components/pages/onboarding/hooks/useEnsureOnboardingResources' import { useOnboardingFlagRename } from 'components/pages/onboarding/hooks/useOnboardingFlagRename' import { useOnboardingFlag } from 'components/pages/onboarding/hooks/useOnboardingFlag' @@ -168,20 +166,20 @@ const OnboardingFlow: FC = () => { history.push(`${base}/features?feature=${flagId}&tab=${tab}`) } - const goToNextStep = (step: OnboardingNextStep) => - step === 'rollout' ? rolloutQuest.open() : goToFlagConfig(step) - const diagnosticIds = { environment_id: environment?.id, organisation_id: organisationId, project_id: projectId, } - const rolloutQuest = useRolloutQuest({ + const openRolloutQuest = useRolloutQuest({ diagnosticIds, featureName, onContinue: () => goToFlagConfig('rollout'), who: { email: profile?.email, organisation: organisationDisplayName }, }) + + const goToNextStep = (step: OnboardingNextStep) => + step === 'rollout' ? openRolloutQuest() : goToFlagConfig(step) const trackSnippetCopied = (snippet: OnboardingSnippet) => API.trackEvent({ ...Constants.events.ONBOARDING_SNIPPET_COPIED, @@ -230,10 +228,6 @@ const OnboardingFlow: FC = () => { ) } - if (rolloutQuest.isOpen) { - return - } - return (

diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss index f1f60640977c..b730a71e74ec 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss @@ -1,10 +1,4 @@ .onboarding-rollout-quest { - max-width: 720px; - - &__title { - font-size: 24px; - } - &__card-title, &__soon-title { font-size: 16px; diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx index 87f6f4224646..2108e18b52f1 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -24,15 +24,10 @@ const OnboardingRolloutQuest: FC = ({ onFeedback, onNotifyMe, }) => ( -
-
-

- Roll out gradually -

-

- Release {featureName} to a growing percentage of your users. -

-
+
+

+ Release {featureName} to a growing percentage of your users. +

diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts index 1fd0fa401a2a..7b4b300073c1 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts @@ -1,12 +1,11 @@ -import { useEffect } from 'react' -import { useHistory, useLocation } from 'react-router-dom' +import React from 'react' import API from 'project/api' import Constants from 'common/constants' import trackRolloutInterest, { ROLLOUT_BETA_REQUESTED, ROLLOUT_FEEDBACK_CLICKED, } from './trackRolloutInterest' -import { OnboardingRolloutQuestProps } from './OnboardingRolloutQuest' +import OnboardingRolloutQuest from './OnboardingRolloutQuest' type UseRolloutQuest = { featureName: string @@ -19,60 +18,43 @@ type UseRolloutQuest = { } /** - * The rollout quest's own routing and analytics, so the flow only has to ask - * whether it is open. + * Opens the gradual rollout quest, with its analytics. * - * Rollout is the one quest with a screen of its own: the segment overrides tab - * alone explains none of the steps a rollout takes. The rest still go straight - * to their config. The open quest lives in the URL, so refresh and back both - * behave. + * Rollout is the one next step that explains itself before sending you on: + * the segment overrides tab alone shows none of the steps a rollout takes. */ const useRolloutQuest = ({ diagnosticIds, featureName, onContinue, who, -}: UseRolloutQuest): { - isOpen: boolean - open: () => void - props: OnboardingRolloutQuestProps -} => { - const history = useHistory() - const location = useLocation() - const isOpen = new URLSearchParams(location.search).get('quest') === 'rollout' - +}: UseRolloutQuest): (() => void) => { const track = (event: { category: string; event: string }) => API.trackEvent({ ...event, extra: diagnosticIds }) - useEffect(() => { - if (!isOpen) return - API.trackEvent({ - ...Constants.events.ONBOARDING_ROLLOUT_VIEWED, - extra: diagnosticIds, - }) - // Once per opening, not on every id settling. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen]) - - return { - isOpen, - open: () => history.push('/getting-started?quest=rollout'), - props: { - featureName, - onContinue: () => { - track(Constants.events.ONBOARDING_ROLLOUT_CONTINUED) - onContinue() - }, - onDismiss: () => history.push('/getting-started'), - onFeedback: () => { - track(Constants.events.ONBOARDING_ROLLOUT_FEEDBACK) - trackRolloutInterest(ROLLOUT_FEEDBACK_CLICKED, who) - }, - onNotifyMe: () => { - track(Constants.events.ONBOARDING_ROLLOUT_NOTIFY_ME) - trackRolloutInterest(ROLLOUT_BETA_REQUESTED, who) - }, - }, + return () => { + track(Constants.events.ONBOARDING_ROLLOUT_VIEWED) + openModal( + 'Roll out gradually', + React.createElement(OnboardingRolloutQuest, { + featureName, + onContinue: () => { + track(Constants.events.ONBOARDING_ROLLOUT_CONTINUED) + closeModal() + onContinue() + }, + onDismiss: () => closeModal(), + onFeedback: () => { + track(Constants.events.ONBOARDING_ROLLOUT_FEEDBACK) + trackRolloutInterest(ROLLOUT_FEEDBACK_CLICKED, who) + }, + onNotifyMe: () => { + track(Constants.events.ONBOARDING_ROLLOUT_NOTIFY_ME) + trackRolloutInterest(ROLLOUT_BETA_REQUESTED, who) + }, + }), + 'p-0 modal--wide', + ) } } From e3b0aa6034d1e5c0e2f26082fed1cf256696e3e3 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 13:00:32 -0300 Subject: [PATCH 09/15] refactor(onboarding): use the modal's own body padding p-0 killed the modal body padding and p-4 added the same value back. Both are $spacer * 1.5, so the two cancelled out. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingRolloutQuest/OnboardingRolloutQuest.tsx | 2 +- .../pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx index 2108e18b52f1..37c2df3a2586 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -24,7 +24,7 @@ const OnboardingRolloutQuest: FC = ({ onFeedback, onNotifyMe, }) => ( -
+

Release {featureName} to a growing percentage of your users.

diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts index 7b4b300073c1..eec90606ecfa 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts @@ -53,7 +53,7 @@ const useRolloutQuest = ({ trackRolloutInterest(ROLLOUT_BETA_REQUESTED, who) }, }), - 'p-0 modal--wide', + 'modal--wide', ) } } From 5969d3816a9ddddd2f3bba8cddaf03b25263f068 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 17:37:52 -0300 Subject: [PATCH 10/15] docs(onboarding): trim the rollout quest comments Most restated the signature or the heading above them. Kept the ones that carry a constraint: the contrast reason for the accent chip, why the mailto has no target, and why the line height is not .lh-base. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingRolloutQuest.scss | 3 +-- .../OnboardingRolloutQuest/RolloutComingSoonCard.tsx | 5 ++--- .../OnboardingRolloutQuest/rolloutSteps.tsx | 5 ++--- .../OnboardingRolloutQuest/trackRolloutInterest.ts | 11 ++--------- .../OnboardingRolloutQuest/useRolloutQuest.ts | 9 ++------- 5 files changed, 9 insertions(+), 24 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss index b730a71e74ec..3b2292208a3a 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss @@ -15,7 +15,7 @@ padding: 24px; } - // Numbered, but the marker is the styled circle below, not a list bullet. + // The marker is the styled circle below, not a list bullet. &__steps { list-style: none; } @@ -31,7 +31,6 @@ font-size: 14px; } - // The icon inherits it via currentColor. &__notified { font-size: 13px; } diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx index 3227bfb21dcd..fbf5543a7ad6 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx @@ -11,9 +11,8 @@ export type RolloutComingSoonCardProps = { onFeedback: () => void } -// The one thing we don't ship yet: doing the three steps above in a single -// action. It promises a simpler flow, never the capability, which already -// exists. +// The one thing we don't ship yet: the three steps above in a single action. It +// promises a simpler flow, never the capability. const RolloutComingSoonCard: FC = ({ onFeedback, onNotifyMe, diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx index 910bc9942b97..a59d4be1bd58 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx @@ -4,7 +4,6 @@ import Icon from 'components/icons/Icon' export const ROLLOUT_GUIDE_URL = 'https://docs.flagsmith.com/managing-flags/rollout/rollout-by-percentage' -// The steps someone takes today, not a workaround: this works right now. export const getRolloutSteps = ( featureName: string, ): { title: string; body: string }[] => [ @@ -22,8 +21,8 @@ export const getRolloutSteps = ( }, ] -// The prerequisite the guide flags. Without it the override reaches nobody and -// the user has no way to tell, so it sits with the steps rather than in docs. +// Sits with the steps rather than in docs: without it the override reaches +// nobody and the user has no way to tell. export const RolloutPrerequisite: FC = () => (

diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts index 28734a7d6ac6..60005ba48510 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts @@ -8,15 +8,8 @@ type RolloutInterest = { organisation?: string } -/** - * Interest in the door, reported where the other fake doors report and - * carrying who asked. The onboarding funnel events record that an - * organisation wants this; these record who to reply to. Same shape as the - * segment sources door. - * - * Takes who rather than reading it, so the caller supplies it from the queries - * it already holds. - */ +// The onboarding funnel events say an organisation wants this; this says who to +// reply to. Sent through flagsmith.trackEvent, same as the segment sources door. const trackRolloutInterest = ( event: string, { email, organisation }: RolloutInterest, diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts index eec90606ecfa..f502085fafbc 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts @@ -11,18 +11,13 @@ type UseRolloutQuest = { featureName: string /** Where "Create a rollout segment" ends up. */ onContinue: () => void - /** Onboarding funnel context, as the other onboarding events send it. */ diagnosticIds: Record /** Who to reply to when someone asks for access. */ who: { email?: string; organisation?: string } } -/** - * Opens the gradual rollout quest, with its analytics. - * - * Rollout is the one next step that explains itself before sending you on: - * the segment overrides tab alone shows none of the steps a rollout takes. - */ +// The segment overrides tab alone shows none of the steps a rollout takes, so +// this opens first. const useRolloutQuest = ({ diagnosticIds, featureName, From 12f87c522d98134b1df5b37d03f7891d20465bc1 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 17:39:16 -0300 Subject: [PATCH 11/15] refactor(onboarding): drop the wrapper spans inside the quest buttons .btn is already inline-flex with gap 0.5rem, so d-inline-flex align-items-center gap-2 on an inner span restated it one node deeper. The guide link now takes .btn-link's own 0.25rem gap. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingRolloutQuest.tsx | 12 ++++-------- .../OnboardingRolloutQuest/RolloutComingSoonCard.tsx | 6 ++---- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx index 37c2df3a2586..72f112aab499 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -57,10 +57,8 @@ const OnboardingRolloutQuest: FC = ({ target='_blank' className='align-self-start' > - - - Read the gradual rollout guide - + + Read the gradual rollout guide

@@ -68,10 +66,8 @@ const OnboardingRolloutQuest: FC = ({
)} {/* No target: a mailto in a new tab leaves a blank tab behind. */} From e738a4385fe20c55ae7b56e1cf55666472eccb6f Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 17:45:03 -0300 Subject: [PATCH 12/15] fix(onboarding): make the rollout steps card visible in light mode surface-default is white in light mode and so is the modal body, so the card had nothing to show. border-default sets only a colour, so there was no border to fall back on either. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingRolloutQuest/OnboardingRolloutQuest.scss | 7 +++++++ .../OnboardingRolloutQuest/OnboardingRolloutQuest.tsx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss index 3b2292208a3a..1cde3306ef39 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss @@ -15,6 +15,13 @@ padding: 24px; } + // The fill alone is near-invisible in dark mode, where the card and the modal + // are a shade apart. .border-default sets only a colour, so the shorthand + // lives here, as it does in MetricsTable and VariationTable. + &__card { + border: 1px solid var(--color-border-default); + } + // The marker is the styled circle below, not a list bullet. &__steps { list-style: none; diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx index 72f112aab499..4ab1be9df02f 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -29,7 +29,7 @@ const OnboardingRolloutQuest: FC = ({ Release {featureName} to a growing percentage of your users.

-
+

How to roll out gradually today

From 82185fc66a105a8764ca12c56a51866b22533368 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Thu, 13 Aug 2026 17:52:50 -0300 Subject: [PATCH 13/15] refactor(onboarding): use the shared spacing and type classes in the quest padding: 24px is p-4, list-style: none is .list-unstyled, and the two 16px headings were h3 elements held down by a font-size, so they still carried h3's 40px line height. As h6 they take size, line height, weight and the dark-mode colour from _type.scss. Co-Authored-By: Claude Opus 5 (1M context) --- .../OnboardingRolloutQuest.scss | 29 ------------------- .../OnboardingRolloutQuest.tsx | 14 ++++----- .../RolloutComingSoonCard.tsx | 8 ++--- .../OnboardingRolloutQuest/rolloutSteps.tsx | 4 +-- 4 files changed, 10 insertions(+), 45 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss index 1cde3306ef39..6f8d829a26f6 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss @@ -1,20 +1,10 @@ .onboarding-rollout-quest { - &__card-title, - &__soon-title { - font-size: 16px; - } - // 1.5 rather than .lh-base, which is 1.375 here. &__body { font-size: 13px; line-height: 1.5; } - &__card, - &__soon { - padding: 24px; - } - // The fill alone is near-invisible in dark mode, where the card and the modal // are a shade apart. .border-default sets only a colour, so the shorthand // lives here, as it does in MetricsTable and VariationTable. @@ -22,32 +12,13 @@ border: 1px solid var(--color-border-default); } - // The marker is the styled circle below, not a list bullet. - &__steps { - list-style: none; - } - &__step-number { width: 24px; height: 24px; - font-size: 12px; - font-weight: 700; - } - - &__step-title { - font-size: 14px; - } - - &__notified { - font-size: 13px; } &__prerequisite { font-size: 12px; line-height: 1.5; - - code { - font-size: 12px; - } } } diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx index 4ab1be9df02f..655c3ca919e2 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -29,20 +29,16 @@ const OnboardingRolloutQuest: FC = ({ Release {featureName} to a growing percentage of your users.

-
-

- How to roll out gradually today -

-
    +
    +
    How to roll out gradually today
    +
      {getRolloutSteps(featureName).map((step, index) => (
    1. - + {index + 1} - - {step.title} - + {step.title} {step.body} diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx index 43c0cf7bf8ff..80cf37134f25 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx @@ -23,23 +23,21 @@ const RolloutComingSoonCard: FC = ({ onNotifyMe() } return ( -
      +
      {/* Accent rather than the design's solid purple: there is no inverse text token, and white on the dark-mode action surface is ~3.2:1. */} Coming soon -

      - We’re making gradual rollouts one-click -

      +
      We’re making gradual rollouts one-click

      Soon you’ll ramp a flag up on a schedule automatically, without editing segments by hand. Want early access?

      {notified ? ( - + Thanks, we’ll be in touch. diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx index a59d4be1bd58..671a4450ef06 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx @@ -28,8 +28,8 @@ export const RolloutPrerequisite: FC = () => ( Percentage splits only apply to users you identify. If your app doesn’t - call flagsmith.identify(...) yet, add it first, or everyone - keeps getting the same value. + call flagsmith.identify(...) yet, + add it first, or everyone keeps getting the same value.

      ) From c53f2e9e3bab812c405292a001a1d3a8a0859102 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 14 Aug 2026 09:02:17 -0300 Subject: [PATCH 14/15] fix(onboarding): address the rollout quest review Gate the quest on onboarding_rollout_quest, so off keeps today's deep link to the overrides tab. Persist the beta request as a trait rather than local state, so it survives a reload and can be targeted later. Tell the info icon to inherit its colour, since it hardcodes blue and Icon drops className for this case. Use fs-caption and fs-captionSmall instead of hardcoded sizes, and rename the tracking metadata's source to origin now that sources means segment sources. Co-Authored-By: Claude Opus 5 (1M context) --- .../onboarding/OnboardingFlow/OnboardingFlow.tsx | 8 +++++++- .../OnboardingRolloutQuest.scss | 11 ----------- .../OnboardingRolloutQuest/OnboardingRolloutQuest.tsx | 4 ++-- .../OnboardingRolloutQuest/RolloutComingSoonCard.tsx | 11 +++++++++-- .../OnboardingRolloutQuest/rolloutSteps.tsx | 5 +++-- .../OnboardingRolloutQuest/trackRolloutInterest.ts | 2 +- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index ade40f851657..f9f499181834 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -178,8 +178,14 @@ const OnboardingFlow: FC = () => { who: { email: profile?.email, organisation: organisationDisplayName }, }) + // Off, rollout deep-links to the overrides tab like every other next step. + const rolloutQuestEnabled = Utils.getFlagsmithHasFeature( + 'onboarding_rollout_quest', + ) const goToNextStep = (step: OnboardingNextStep) => - step === 'rollout' ? openRolloutQuest() : goToFlagConfig(step) + step === 'rollout' && rolloutQuestEnabled + ? openRolloutQuest() + : goToFlagConfig(step) const trackSnippetCopied = (snippet: OnboardingSnippet) => API.trackEvent({ ...Constants.events.ONBOARDING_SNIPPET_COPIED, diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss index 6f8d829a26f6..964a09899f44 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.scss @@ -1,10 +1,4 @@ .onboarding-rollout-quest { - // 1.5 rather than .lh-base, which is 1.375 here. - &__body { - font-size: 13px; - line-height: 1.5; - } - // The fill alone is near-invisible in dark mode, where the card and the modal // are a shade apart. .border-default sets only a colour, so the shorthand // lives here, as it does in MetricsTable and VariationTable. @@ -16,9 +10,4 @@ width: 24px; height: 24px; } - - &__prerequisite { - font-size: 12px; - line-height: 1.5; - } } diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx index 655c3ca919e2..bec3451a93fb 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/OnboardingRolloutQuest.tsx @@ -25,7 +25,7 @@ const OnboardingRolloutQuest: FC = ({ onNotifyMe, }) => (
      -

      +

      Release {featureName} to a growing percentage of your users.

      @@ -39,7 +39,7 @@ const OnboardingRolloutQuest: FC = ({ {step.title} - + {step.body} diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx index 80cf37134f25..c8d6f11b76a5 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx @@ -1,7 +1,9 @@ import React, { FC, useState } from 'react' +import flagsmith from '@flagsmith/flagsmith' import Button from 'components/base/forms/Button' import Chip from 'components/base/Chip' import Icon from 'components/icons/Icon' +import { ROLLOUT_BETA_REQUESTED } from './trackRolloutInterest' export const ROLLOUT_FEEDBACK_URL = 'mailto:support@flagsmith.com?subject=Gradual%20rollout' @@ -17,9 +19,14 @@ const RolloutComingSoonCard: FC = ({ onFeedback, onNotifyMe, }) => { + // A trait rather than local state, so asking survives a reload and the beta + // can later be handed out by targeting it. Read at render, as Announcement + // does, so it still resolves if traits land after mount. const [notified, setNotified] = useState(false) + const alreadyAsked = notified || !!flagsmith.getTrait(ROLLOUT_BETA_REQUESTED) const notifyMe = () => { setNotified(true) + flagsmith.setTrait(ROLLOUT_BETA_REQUESTED, true) onNotifyMe() } return ( @@ -31,12 +38,12 @@ const RolloutComingSoonCard: FC = ({ Coming soon
      We’re making gradual rollouts one-click
      -

      +

      Soon you’ll ramp a flag up on a schedule automatically, without editing segments by hand. Want early access?

      - {notified ? ( + {alreadyAsked ? ( Thanks, we’ll be in touch. diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx index 671a4450ef06..27f35251f8bc 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/rolloutSteps.tsx @@ -24,8 +24,9 @@ export const getRolloutSteps = ( // Sits with the steps rather than in docs: without it the override reaches // nobody and the user has no way to tell. export const RolloutPrerequisite: FC = () => ( -

      - +

      + {/* info hardcodes a blue fill, so it needs telling to inherit. */} + Percentage splits only apply to users you identify. If your app doesn’t call flagsmith.identify(...) yet, diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts index 60005ba48510..3e6f802ef661 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/trackRolloutInterest.ts @@ -18,7 +18,7 @@ const trackRolloutInterest = ( metadata: { email, organisation, - source: 'onboarding-rollout-quest', + origin: 'onboarding-rollout-quest', }, }) } From 7b9d19cf10b864a2cb6ee4cce9acf953120de41d Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 14 Aug 2026 09:20:56 -0300 Subject: [PATCH 15/15] copy(onboarding): addressing copy feedbacks --- .../pages/onboarding/OnboardingFlow/OnboardingFlow.tsx | 2 +- .../OnboardingRolloutQuest/RolloutComingSoonCard.tsx | 4 ++-- .../onboarding/OnboardingRolloutQuest/useRolloutQuest.ts | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx index f9f499181834..2c2ea46f7033 100644 --- a/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingFlow/OnboardingFlow.tsx @@ -1,4 +1,4 @@ -import React, { FC, useState } from 'react' +import React, { FC, useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' import Button from 'components/base/forms/Button' import Icon from 'components/icons/Icon' diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx index c8d6f11b76a5..5fbc605038cd 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/RolloutComingSoonCard.tsx @@ -39,8 +39,8 @@ const RolloutComingSoonCard: FC = ({

      We’re making gradual rollouts one-click

      - Soon you’ll ramp a flag up on a schedule automatically, without editing - segments by hand. Want early access? + Automatically release according to a schedule, without manual editing of + segments. Want early access?

      {alreadyAsked ? ( diff --git a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts index f502085fafbc..63604674356a 100644 --- a/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts +++ b/frontend/web/components/pages/onboarding/OnboardingRolloutQuest/useRolloutQuest.ts @@ -30,7 +30,8 @@ const useRolloutQuest = ({ return () => { track(Constants.events.ONBOARDING_ROLLOUT_VIEWED) openModal( - 'Roll out gradually', + // Matches the next-step card that opens it. + 'Gradual rollout', React.createElement(OnboardingRolloutQuest, { featureName, onContinue: () => {