feat: add feature flag modal in local/staging environments (#4291)

* initial

* add feature flag modal

* updates

* help

* working now!

* SIMPLIFY

* useref delete

* naming

* move modal out

* rename

* make button

* rename

* work on vercel

* styling

Co-authored-by: Vignesh Mohankumar <me@vig.xyz>
This commit is contained in:
Kaylee George 2022-08-05 17:39:20 -04:00 committed by GitHub
parent 25ea7f9caf
commit 51aa02d878
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 147 additions and 8 deletions

@ -0,0 +1,110 @@
import { FeatureFlag, useUpdateFlag } from 'featureFlags'
import { Phase0Variant, usePhase0Flag } from 'featureFlags/flags/phase0'
import { ReactNode } from 'react'
import { X } from 'react-feather'
import { useModalIsOpen, useToggleFeatureFlags } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled from 'styled-components/macro'
const StyledModal = styled.div`
position: fixed;
display: flex;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 400px;
height: fit-content;
color: ${({ theme }) => theme.textPrimary};
font-size: 18px;
padding: 20px;
background-color: ${({ theme }) => theme.backgroundSurface};
border-radius: 12px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
z-index: 100;
flex-direction: column;
gap: 8px;
border: 1px solid ${({ theme }) => theme.backgroundOutline};
`
function Modal({ open, children }: { open: boolean; children: ReactNode }) {
return open ? <StyledModal>{children}</StyledModal> : null
}
const Row = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0px;
`
const CloseButton = styled.button`
cursor: pointer;
background: ${({ theme }) => theme.none};
border: none;
color: ${({ theme }) => theme.textPrimary};
`
const Header = styled(Row)`
font-weight: 600;
font-size: 20px;
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
margin-bottom: 8px;
`
function Variant({ option }: { option: string }) {
return <option value={option}>{option}</option>
}
function FeatureFlagOption({
variants,
featureFlag,
value,
label,
}: {
variants: string[]
featureFlag: FeatureFlag
value: string
label: string
}) {
const updateFlag = useUpdateFlag()
return (
<Row key={featureFlag}>
{featureFlag}: {label}
<select
id={featureFlag}
value={value}
onChange={(e) => {
updateFlag(featureFlag, e.target.value)
window.location.reload()
}}
>
{variants.map((variant) => (
<Variant key={variant} option={variant} />
))}
</select>
</Row>
)
}
export default function FeatureFlagModal() {
const open = useModalIsOpen(ApplicationModal.FEATURE_FLAGS)
const toggle = useToggleFeatureFlags()
return (
<Modal open={open}>
<Header>
Feature Flag Settings
<CloseButton onClick={toggle}>
<X size={24} />
</CloseButton>
</Header>
<FeatureFlagOption
variants={Object.values(Phase0Variant)}
value={usePhase0Flag()}
featureFlag={FeatureFlag.phase0}
label="All Phase 0 changes (redesign, explore, header)."
/>
</Modal>
)
}

@ -1,6 +1,7 @@
// eslint-disable-next-line no-restricted-imports
import { t, Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal'
import { PrivacyPolicyModal } from 'components/PrivacyPolicy'
import { L2_CHAIN_IDS } from 'constants/chains'
import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales'
@ -214,6 +215,7 @@ export default function Menu() {
const toggleMenu = useToggleModal(ApplicationModal.MENU)
useOnClickOutside(node, open ? toggleMenu : undefined)
const togglePrivacyPolicy = useToggleModal(ApplicationModal.PRIVACY_POLICY)
const openFeatureFlagsModal = useToggleModal(ApplicationModal.FEATURE_FLAGS)
const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM)
const showUNIClaimOption = Boolean(!!account && !!chainId && !L2_CHAIN_IDS.includes(chainId))
@ -299,12 +301,16 @@ export default function Menu() {
<Trans>Claim UNI</Trans>
</UNIbutton>
)}
{['development', 'staging'].includes(process.env.NODE_ENV) && (
<ToggleMenuItem onClick={openFeatureFlagsModal}>Feature Flags</ToggleMenuItem>
)}
</MenuFlyout>
)
}
})()}
</StyledMenu>
<PrivacyPolicyModal />
<FeatureFlagModal />
</>
)
}

@ -1,4 +1,6 @@
import { createContext, ReactNode, useContext } from 'react'
import { useAtom } from 'jotai'
import { atomWithStorage, useAtomValue } from 'jotai/utils'
import { createContext, ReactNode, useCallback, useContext } from 'react'
interface FeatureFlagsContextType {
isLoaded: boolean
@ -16,16 +18,28 @@ export function useFeatureFlagsContext(): FeatureFlagsContextType {
}
}
/* update and save feature flag settings */
export const featureFlagSettings = atomWithStorage<Record<string, string>>('featureFlags', {})
export function useUpdateFlag() {
const [featureFlags, setFeatureFlags] = useAtom(featureFlagSettings)
return useCallback(
(featureFlag: string, option: string) => {
featureFlags[featureFlag] = option
setFeatureFlags(featureFlags)
},
[featureFlags, setFeatureFlags]
)
}
export function FeatureFlagsProvider({ children }: { children: ReactNode }) {
// TODO(vm): `isLoaded` to `true` so `App.tsx` will render. Later, this will be dependent on
// flags loading from Amplitude, with a timeout.
const variant = process.env.NODE_ENV === 'development' ? 'enabled' : 'control'
const featureFlags = useAtomValue(featureFlagSettings)
const value = {
isLoaded: true,
flags: {
phase0: variant,
phase1: variant,
},
flags: featureFlags,
}
return <FeatureFlagContext.Provider value={value}>{children}</FeatureFlagContext.Provider>
}
@ -35,8 +49,12 @@ export function useFeatureFlagsIsLoaded(): boolean {
}
export enum BaseVariant {
Control = 'Control',
Enabled = 'Enabled',
Control = 'control',
Enabled = 'enabled',
}
export enum FeatureFlag {
phase0 = 'phase0',
}
export function useBaseFlag(flag: string): BaseVariant {

@ -70,6 +70,10 @@ export function useToggleTimeSelector(): () => void {
return useToggleModal(ApplicationModal.TIME_SELECTOR)
}
export function useToggleFeatureFlags(): () => void {
return useToggleModal(ApplicationModal.FEATURE_FLAGS)
}
// returns a function that allows adding a popup
export function useAddPopup(): (content: PopupContent, key?: string, removeAfterMs?: number) => void {
const dispatch = useAppDispatch()

@ -31,6 +31,7 @@ export enum ApplicationModal {
TIME_SELECTOR,
SHARE,
NETWORK_FILTER,
FEATURE_FLAGS,
}
type PopupList = Array<{ key: string; show: boolean; content: PopupContent; removeAfterMs: number | null }>