-
Notifications
You must be signed in to change notification settings - Fork 557
feat(onboarding): gradual rollout quest screen #8286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a137d58
3e462e1
73e490e
924fdb8
b353bf3
af5afae
c4c2707
01dcb9e
e3b0aa6
5969d38
12f87c5
e738a43
82185fc
c53f2e9
7b9d19c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| .onboarding-rollout-quest { | ||
| // 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); | ||
| } | ||
|
|
||
| &__step-number { | ||
| width: 24px; | ||
| height: 24px; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| 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 | ||
| onContinue: () => void | ||
| onDismiss: () => void | ||
| onNotifyMe: () => void | ||
| onFeedback: () => void | ||
| } | ||
|
|
||
| const OnboardingRolloutQuest: FC<OnboardingRolloutQuestProps> = ({ | ||
| featureName, | ||
| onContinue, | ||
| onDismiss, | ||
| onFeedback, | ||
| onNotifyMe, | ||
| }) => ( | ||
| <div className='onboarding-rollout-quest d-flex flex-column gap-4'> | ||
| <p className='fs-caption lh-sm text-secondary m-0'> | ||
| Release {featureName} to a growing percentage of your users. | ||
| </p> | ||
|
|
||
| <section className='onboarding-rollout-quest__card bg-surface-muted rounded-xl p-4 d-flex flex-column gap-3'> | ||
| <h6 className='m-0'>How to roll out gradually today</h6> | ||
| <ol className='list-unstyled d-flex flex-column gap-3 m-0'> | ||
| {getRolloutSteps(featureName).map((step, index) => ( | ||
| <li key={step.title} className='d-flex gap-3'> | ||
| <span className='onboarding-rollout-quest__step-number bg-surface-action-subtle text-action rounded-full fs-captionSmall fw-bold d-flex align-items-center justify-content-center flex-shrink-0'> | ||
| {index + 1} | ||
| </span> | ||
| <span className='d-flex flex-column gap-1'> | ||
| <span className='fw-bold text-default'>{step.title}</span> | ||
| <span className='fs-caption lh-sm text-secondary'> | ||
| {step.body} | ||
| </span> | ||
| </span> | ||
| </li> | ||
| ))} | ||
| </ol> | ||
| <RolloutPrerequisite /> | ||
| <Button | ||
| theme='text' | ||
| href={ROLLOUT_GUIDE_URL} | ||
| target='_blank' | ||
| className='align-self-start' | ||
| > | ||
| <Icon name='file-text' width={14} /> | ||
| Read the gradual rollout guide | ||
| </Button> | ||
| </section> | ||
|
|
||
| <RolloutComingSoonCard onNotifyMe={onNotifyMe} onFeedback={onFeedback} /> | ||
|
|
||
| <div className='d-flex align-items-center gap-3'> | ||
| <Button onClick={onContinue}> | ||
| <Icon name='layers' width={14} /> | ||
| Create a rollout segment | ||
| </Button> | ||
| <Button theme='text' onClick={onDismiss}> | ||
| Maybe later | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| ) | ||
|
|
||
| export default OnboardingRolloutQuest |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| 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' | ||
|
|
||
| export type RolloutComingSoonCardProps = { | ||
| onNotifyMe: () => void | ||
| onFeedback: () => void | ||
| } | ||
|
|
||
| // 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<RolloutComingSoonCardProps> = ({ | ||
| 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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't done it with the sources fake door but I believe setting a trait and using the trait as a persisted state is actually better, especially if we want to use it to access the beta in the future
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call, done in 963519c. Reused the event name as the trait key so there's one name for the thing: const alreadyAsked = notified || !!flagsmith.getTrait(ROLLOUT_BETA_REQUESTED) |
||
| const alreadyAsked = notified || !!flagsmith.getTrait(ROLLOUT_BETA_REQUESTED) | ||
| const notifyMe = () => { | ||
| setNotified(true) | ||
| flagsmith.setTrait(ROLLOUT_BETA_REQUESTED, true) | ||
| onNotifyMe() | ||
| } | ||
| return ( | ||
| <section className='bg-surface-action-subtle rounded-xl p-4 d-flex flex-column gap-2'> | ||
| {/* 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. */} | ||
| <Chip variant='accent' size='xs' className='align-self-start'> | ||
| <Icon name='flash' width={12} /> | ||
| Coming soon | ||
| </Chip> | ||
| <h6 className='m-0'>We’re making gradual rollouts one-click</h6> | ||
| <p className='fs-caption lh-sm text-secondary m-0'> | ||
| Automatically release according to a schedule, without manual editing of | ||
| segments. Want early access? | ||
| </p> | ||
| <div className='d-flex align-items-center gap-3'> | ||
| {alreadyAsked ? ( | ||
| <span className='fs-caption d-inline-flex align-items-center gap-2 text-action'> | ||
| <Icon name='checkmark-circle' width={16} /> | ||
| Thanks, we’ll be in touch. | ||
| </span> | ||
| ) : ( | ||
| <Button onClick={notifyMe}> | ||
| <Icon name='bell' width={14} /> | ||
| Notify me | ||
| </Button> | ||
| )} | ||
| {/* No target: a mailto in a new tab leaves a blank tab behind. */} | ||
| <Button theme='text' href={ROLLOUT_FEEDBACK_URL} onClick={onFeedback}> | ||
| Tell us what you need | ||
| </Button> | ||
| </div> | ||
| </section> | ||
| ) | ||
| } | ||
|
|
||
| export default RolloutComingSoonCard | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export { default } from './OnboardingRolloutQuest' | ||
| export type { OnboardingRolloutQuestProps } from './OnboardingRolloutQuest' | ||
| export { default as useRolloutQuest } from './useRolloutQuest' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| 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' | ||
|
|
||
| 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', | ||
| }, | ||
| ] | ||
|
|
||
| // 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 = () => ( | ||
| <p className='fs-captionSmall lh-sm text-secondary d-flex gap-2 m-0'> | ||
| {/* info hardcodes a blue fill, so it needs telling to inherit. */} | ||
| <Icon name='info' width={14} fill='currentColor' /> | ||
| <span> | ||
| Percentage splits only apply to users you identify. If your app doesn’t | ||
| call <code className='fs-captionSmall'>flagsmith.identify(...)</code> yet, | ||
| add it first, or everyone keeps getting the same value. | ||
| </span> | ||
| </p> | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| 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 | ||
| } | ||
|
|
||
| // 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, | ||
| ): void => { | ||
| flagsmith.trackEvent(event, { | ||
| metadata: { | ||
| email, | ||
| organisation, | ||
| origin: 'onboarding-rollout-quest', | ||
| }, | ||
| }) | ||
| } | ||
|
|
||
| export default trackRolloutInterest |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,57 @@ | ||||||||||
| 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 OnboardingRolloutQuest from './OnboardingRolloutQuest' | ||||||||||
|
|
||||||||||
| type UseRolloutQuest = { | ||||||||||
| featureName: string | ||||||||||
| /** Where "Create a rollout segment" ends up. */ | ||||||||||
| onContinue: () => void | ||||||||||
| diagnosticIds: Record<string, unknown> | ||||||||||
| /** Who to reply to when someone asks for access. */ | ||||||||||
| who: { email?: string; organisation?: string } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // The segment overrides tab alone shows none of the steps a rollout takes, so | ||||||||||
| // this opens first. | ||||||||||
| const useRolloutQuest = ({ | ||||||||||
| diagnosticIds, | ||||||||||
| featureName, | ||||||||||
| onContinue, | ||||||||||
| who, | ||||||||||
| }: UseRolloutQuest): (() => void) => { | ||||||||||
| const track = (event: { category: string; event: string }) => | ||||||||||
| API.trackEvent({ ...event, extra: diagnosticIds }) | ||||||||||
|
|
||||||||||
| return () => { | ||||||||||
| track(Constants.events.ONBOARDING_ROLLOUT_VIEWED) | ||||||||||
| openModal( | ||||||||||
| // Matches the next-step card that opens it. | ||||||||||
| 'Gradual rollout', | ||||||||||
|
Comment on lines
+33
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Keep the modal title aligned with the specified copy. The PR objective and supplied designs require the title “Roll out gradually”. Line 34 uses “Gradual rollout” instead. Restore the specified title, or update the product copy and designs in the same change if the new wording is intentional. Proposed fix // Matches the next-step card that opens it.
- 'Gradual rollout',
+ 'Roll out gradually',📝 Committable suggestion
Suggested change
|
||||||||||
| 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) | ||||||||||
| }, | ||||||||||
| }), | ||||||||||
| 'modal--wide', | ||||||||||
| ) | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export default useRolloutQuest | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Import
Utilsbefore reading the rollout feature flag.Lines 164-166 reference an unbound
Utilsidentifier. TypeScript compilation fails beforeOnboardingFlowcan render.Proposed fix
import { useGetProfileQuery } from 'common/services/useProfile' +import Utils from 'common/utils/utils' import API from 'project/api'📝 Committable suggestion