Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Add Feature Flag System (#10099)
Browse files Browse the repository at this point in the history
* Add service and state for feature flag system

* license

* Add feature flags for emote and social menus

* replace object with key and value store

* remove seed import

* update feature flags to respect hierarchy

* add flags for avaturn and rpm

* format

* optimize reactor

* fix

---------

Co-authored-by: Hanzla Mateen <[email protected]>
  • Loading branch information
HexaField and hanzlamateen authored May 21, 2024
1 parent 3b22b14 commit a536c72
Show file tree
Hide file tree
Showing 14 changed files with 641 additions and 41 deletions.
33 changes: 23 additions & 10 deletions packages/client-core/src/networking/NetworkInstanceProvisioning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,16 @@ import {
} from '@etherealengine/client-core/src/common/services/MediaInstanceConnectionService'
import { ChannelService, ChannelState } from '@etherealengine/client-core/src/social/services/ChannelService'
import { LocationState } from '@etherealengine/client-core/src/social/services/LocationService'
import { InstanceID, LocationID, RoomCode } from '@etherealengine/common/src/schema.type.module'
import { FeatureFlag, InstanceID, LocationID, RoomCode } from '@etherealengine/common/src/schema.type.module'
import { getMutableState, getState, none, useHookstate, useMutableState } from '@etherealengine/hyperflux'
import { NetworkState } from '@etherealengine/network'

import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { FriendService } from '../social/services/FriendService'
import { connectToInstance } from '../transports/SocketWebRTCClientFunctions'
import { PopupMenuState } from '../user/components/UserMenu/PopupMenuService'
import FriendsMenu from '../user/components/UserMenu/menus/FriendsMenu'
import MessagesMenu from '../user/components/UserMenu/menus/MessagesMenu'
import { PopupMenuState } from '../user/components/UserMenu/PopupMenuService'

export const WorldInstanceProvisioning = () => {
const locationState = useMutableState(LocationState)
Expand Down Expand Up @@ -243,33 +244,45 @@ export const SocialMenus = {
Messages: 'Messages'
}

const SocialMenuFlag = 'ir.client.menu.social' as FeatureFlag

export const FriendMenus = () => {
const { t } = useTranslation()
FriendService.useAPIListeners()

const socialsEnabled = FeatureFlagsState.useEnabled(SocialMenuFlag)

useEffect(() => {
const menuState = getMutableState(PopupMenuState)
menuState.menus.merge({
if (!socialsEnabled) return

const popupMenuState = getMutableState(PopupMenuState)
popupMenuState.menus.merge({
[SocialMenus.Friends]: FriendsMenu,
[SocialMenus.Messages]: MessagesMenu
})
menuState.hotbar.merge({

popupMenuState.hotbar.merge({
[SocialMenus.Friends]: { icon: <Groups />, tooltip: t('user:menu.friends') }
})

return () => {
menuState.menus.merge({
popupMenuState.menus.merge({
[SocialMenus.Friends]: none,
[SocialMenus.Messages]: none
})

menuState.hotbar.merge({
popupMenuState.hotbar.merge({
[SocialMenus.Friends]: none
})
}
}, [])
}, [socialsEnabled])

return null
if (!socialsEnabled) return null

const UseFriendsListeners = () => {
FriendService.useAPIListeners()
return null
}
return <UseFriendsListeners />
}

export const InstanceProvisioning = () => {
Expand Down
83 changes: 70 additions & 13 deletions packages/client-core/src/user/UserUISystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ import { defineSystem } from '@etherealengine/ecs/src/SystemFunctions'
import { PresentationSystemGroup } from '@etherealengine/ecs/src/SystemGroups'
import { getMutableState, none } from '@etherealengine/hyperflux'

import { FeatureFlag } from '@etherealengine/common/src/schema.type.module'
import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { InviteService } from '../social/services/InviteService'
import { PopupMenuState } from './components/UserMenu/PopupMenuService'
import AvatarCreatorMenu, { SupportedSdks } from './components/UserMenu/menus/AvatarCreatorMenu'
import AvatarModifyMenu from './components/UserMenu/menus/AvatarModifyMenu'
import AvatarSelectMenu from './components/UserMenu/menus/AvatarSelectMenu'
import EmoteMenu from './components/UserMenu/menus/EmoteMenu'
import ProfileMenu from './components/UserMenu/menus/ProfileMenu'
import SettingMenu from './components/UserMenu/menus/SettingMenu'
import ShareMenu from './components/UserMenu/menus/ShareMenu'
import { PopupMenuState } from './components/UserMenu/PopupMenuService'

export const EmoteIcon = () => (
<svg width="35px" height="35px" viewBox="0 0 184 184" version="1.1">
Expand All @@ -64,10 +66,18 @@ export const UserMenus = {
Emote: 'user.Emote'
}

export const EmoteMenuFlag = 'ir.client.menu.emote' as FeatureFlag
export const AvaturnMenuFlag = 'ir.client.menu.avaturn' as FeatureFlag
export const RPMMenuFlag = 'ir.client.menu.readyPlayerMe' as FeatureFlag

const reactor = () => {
const { t } = useTranslation()
InviteService.useAPIListeners()

const emotesEnabled = FeatureFlagsState.useEnabled(EmoteMenuFlag)
const avaturnEnabled = FeatureFlagsState.useEnabled(AvaturnMenuFlag)
const rpmEnabled = FeatureFlagsState.useEnabled(RPMMenuFlag)

useEffect(() => {
const FaceRetouchingNatural = lazy(() => import('@mui/icons-material/FaceRetouchingNatural'))
const Send = lazy(() => import('@mui/icons-material/Send'))
Expand All @@ -78,16 +88,12 @@ const reactor = () => {
[UserMenus.Settings]: SettingMenu,
[UserMenus.AvatarSelect]: AvatarSelectMenu,
[UserMenus.AvatarModify]: AvatarModifyMenu,
[UserMenus.ReadyPlayer]: AvatarCreatorMenu(SupportedSdks.ReadyPlayerMe),
[UserMenus.Avaturn]: AvatarCreatorMenu(SupportedSdks.Avaturn),
[UserMenus.Share]: ShareMenu,
[UserMenus.Emote]: EmoteMenu
[UserMenus.Share]: ShareMenu
})

popupMenuState.hotbar.merge({
[UserMenus.Profile]: { icon: <FaceRetouchingNatural />, tooltip: t('user:menu.settings') },
[UserMenus.Share]: { icon: <Send />, tooltip: t('user:menu.sendLocation') },
[UserMenus.Emote]: { icon: <EmoteIcon />, tooltip: t('user:menu.emote') }
[UserMenus.Share]: { icon: <Send />, tooltip: t('user:menu.sendLocation') }
})

return () => {
Expand All @@ -96,19 +102,70 @@ const reactor = () => {
[UserMenus.Settings]: none,
[UserMenus.AvatarSelect]: none,
[UserMenus.AvatarModify]: none,
[UserMenus.ReadyPlayer]: none,
[UserMenus.Avaturn]: none,
[UserMenus.Share]: none,
[UserMenus.Emote]: none
[UserMenus.Share]: none
})

popupMenuState.hotbar.merge({
[UserMenus.Profile]: none,
[UserMenus.Share]: none,
[UserMenus.Emote]: none
[UserMenus.Share]: none
})
}
}, [])

useEffect(() => {
if (!emotesEnabled) return

const popupMenuState = getMutableState(PopupMenuState)

popupMenuState.menus.merge({
[UserMenus.Emote]: EmoteMenu
})

popupMenuState.hotbar.merge({
[UserMenus.Emote]: { icon: <EmoteIcon />, tooltip: t('user:menu.emote') }
})

return () => {
popupMenuState.menus.merge({
[UserMenus.Emote]: none
})

popupMenuState.hotbar.merge({
[UserMenus.Emote]: none
})
}
}, [emotesEnabled])

useEffect(() => {
if (!avaturnEnabled) return

const popupMenuState = getMutableState(PopupMenuState)

popupMenuState.menus.merge({
[UserMenus.ReadyPlayer]: AvatarCreatorMenu(SupportedSdks.ReadyPlayerMe)
})
return () => {
popupMenuState.menus.merge({
[UserMenus.ReadyPlayer]: none
})
}
}, [avaturnEnabled])

useEffect(() => {
if (!rpmEnabled) return

const popupMenuState = getMutableState(PopupMenuState)

popupMenuState.menus.merge({
[UserMenus.Avaturn]: AvatarCreatorMenu(SupportedSdks.Avaturn)
})
return () => {
popupMenuState.menus.merge({
[UserMenus.Avaturn]: none
})
}
}, [rpmEnabled])

return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ import Grid from '@etherealengine/ui/src/primitives/mui/Grid'
import Icon from '@etherealengine/ui/src/primitives/mui/Icon'
import IconButton from '@etherealengine/ui/src/primitives/mui/IconButton'

import { FeatureFlagsState } from '@etherealengine/engine/src/FeatureFlagsState'
import { AvaturnMenuFlag, RPMMenuFlag, UserMenus } from '../../../UserUISystem'
import { AvatarService } from '../../../services/AvatarService'
import { UserMenus } from '../../../UserUISystem'
import styles from '../index.module.scss'
import { PopupMenuServices } from '../PopupMenuService'
import styles from '../index.module.scss'

interface Props {
selectedAvatar?: AvatarType
Expand Down Expand Up @@ -83,6 +84,8 @@ const AvatarModifyMenu = ({ selectedAvatar }: Props) => {
const [isSaving, setIsSaving] = useState(false)
const avatarRef = useRef<HTMLInputElement | null>(null)
const thumbnailRef = useRef<HTMLInputElement | null>(null)
const avaturnEnabled = FeatureFlagsState.useEnabled(AvaturnMenuFlag)
const rpmEnabled = FeatureFlagsState.useEnabled(RPMMenuFlag)

let thumbnailSrc = state.thumbnailUrl
if (state.thumbnailFile) {
Expand Down Expand Up @@ -346,23 +349,27 @@ const AvatarModifyMenu = ({ selectedAvatar }: Props) => {
</Grid>

<Grid item md={5} sx={{ width: '100%' }}>
<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.ReadyPlayer)}
>
{t('user:usermenu.profile.useReadyPlayerMe')}
</Button>
{rpmEnabled && (
<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.ReadyPlayer)}
>
{t('user:usermenu.profile.useReadyPlayerMe')}
</Button>
)}

<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.Avaturn)}
>
{t('user:usermenu.profile.useAvaturn')}
</Button>
{avaturnEnabled && (
<Button
fullWidth
type="gradientRounded"
sx={{ mt: 1 }}
onClick={() => PopupMenuServices.showPopupMenu(UserMenus.Avaturn)}
>
{t('user:usermenu.profile.useAvaturn')}
</Button>
)}

<InputText
name="name"
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/schema.type.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export type * from './schemas/setting/chargebee-setting.schema'
export type * from './schemas/setting/client-setting.schema'
export type * from './schemas/setting/coil-setting.schema'
export type * from './schemas/setting/email-setting.schema'
export type * from './schemas/setting/feature-flag-setting.schema'
export type * from './schemas/setting/helm-setting.schema'
export type * from './schemas/setting/instance-server-setting.schema'
export type * from './schemas/setting/redis-setting.schema'
Expand Down Expand Up @@ -224,6 +225,8 @@ export const taskServerSettingPath = 'task-server-setting'

export const emailSettingPath = 'email-setting'

export const featureFlagSettingPath = 'feature-flag-setting'

export const instanceServerSettingPath = 'instance-server-setting'

export const clientSettingPath = 'client-setting'
Expand Down
87 changes: 87 additions & 0 deletions packages/common/src/schemas/setting/feature-flag-setting.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
CPAL-1.0 License
The contents of this file are subject to the Common Public Attribution License
Version 1.0. (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE.
The License is based on the Mozilla Public License Version 1.1, but Sections 14
and 15 have been added to cover use of software over a computer network and
provide for limited attribution for the Original Developer. In addition,
Exhibit A has been modified to be consistent with Exhibit B.
Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
specific language governing rights and limitations under the License.
The Original Code is Ethereal Engine.
The Original Developer is the Initial Developer. The Initial Developer of the
Original Code is the Ethereal Engine team.
All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023
Ethereal Engine. All Rights Reserved.
*/

// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html
import type { Static } from '@feathersjs/typebox'
import { getValidator, querySyntax, Type } from '@feathersjs/typebox'
import { OpaqueType } from '../../interfaces/OpaqueType'
import { TypedString } from '../../types/TypeboxUtils'
import { dataValidator, queryValidator } from '../validators'

export type FeatureFlag = OpaqueType<'FeatureFlag'> & string

export const featureFlagSettingPath = 'feature-flag-setting'

export const featureFlagSettingMethods = ['find', 'get', 'create', 'patch', 'remove'] as const

// Main data model schema
export const featureFlagSettingSchema = Type.Object(
{
id: Type.String({
format: 'uuid'
}),
flagName: TypedString<FeatureFlag>(),
flagValue: Type.Boolean(),
createdAt: Type.String({ format: 'date-time' }),
updatedAt: Type.String({ format: 'date-time' })
},
{ $id: 'FeatureFlagSetting', additionalProperties: false }
)
export interface FeatureFlagSettingType extends Static<typeof featureFlagSettingSchema> {}

// Schema for creating new entries
export const featureFlagSettingDataSchema = Type.Pick(featureFlagSettingSchema, ['flagName', 'flagValue'], {
$id: 'FeatureFlagSettingData'
})
export interface FeatureFlagSettingData extends Static<typeof featureFlagSettingDataSchema> {}

// Schema for updating existing entries
export const featureFlagSettingPatchSchema = Type.Partial(featureFlagSettingSchema, {
$id: 'FeatureFlagSettingPatch'
})
export interface FeatureFlagSettingPatch extends Static<typeof featureFlagSettingPatchSchema> {}

// Schema for allowed query properties
export const featureFlagSettingQueryProperties = Type.Pick(featureFlagSettingSchema, ['id', 'flagName', 'flagValue'])
export const featureFlagSettingQuerySchema = Type.Intersect(
[
querySyntax(featureFlagSettingQueryProperties),
// Add additional query properties here
Type.Object({}, { additionalProperties: false })
],
{ additionalProperties: false }
)
export interface FeatureFlagSettingQuery extends Static<typeof featureFlagSettingQuerySchema> {}

export const featureFlagSettingValidator = /* @__PURE__ */ getValidator(featureFlagSettingSchema, dataValidator)
export const featureFlagSettingDataValidator = /* @__PURE__ */ getValidator(featureFlagSettingDataSchema, dataValidator)
export const featureFlagSettingPatchValidator = /* @__PURE__ */ getValidator(
featureFlagSettingPatchSchema,
dataValidator
)
export const featureFlagSettingQueryValidator = /* @__PURE__ */ getValidator(
featureFlagSettingQuerySchema,
queryValidator
)
1 change: 1 addition & 0 deletions packages/engine/src/EngineModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Ethereal Engine. All Rights Reserved.
/** World Module */
import '@etherealengine/spatial'

export * from './FeatureFlagsState'
export * from './avatar/AvatarModule'
export * from './interaction/InteractionModule'
export * from './interaction/MediaModule'
Expand Down
Loading

0 comments on commit a536c72

Please sign in to comment.