diff --git a/src/analytics/features/index.ts b/src/analytics/features/index.ts
index 67ea7bb4ad..b83ac9e33b 100644
--- a/src/analytics/features/index.ts
+++ b/src/analytics/features/index.ts
@@ -40,6 +40,7 @@ const TIMEOUT_PREFER_FRESH_GATES = 1500
export const features = new GrowthBook({
apiHost: env.GROWTHBOOK_API_HOST,
clientKey: env.GROWTHBOOK_CLIENT_KEY,
+ enableDevMode: env.IS_INTERNAL,
})
/**
@@ -59,8 +60,7 @@ export const init = features.init({timeout: TIMEOUT_INIT}).then(res => {
})
/**
- * Refresh feature gates from GrowthBook. Updates attributes based on the
- * provided account, if any.
+ * Refresh feature gates from GrowthBook.
*/
export async function refresh({strategy}: {strategy: FeatureFetchStrategy}) {
await features.refreshFeatures({
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index a58b1bf3dd..37d0e9b5fa 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -27,6 +27,7 @@ import {useOnboardingDispatch} from '#/state/shell'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useCloseAllActiveElements} from '#/state/util'
import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {GrowthbookDialog} from '#/screens/Settings/components/GrowthbookDialog'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf'
import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice'
@@ -398,6 +399,7 @@ function DevOptions() {
currentChannel,
} = useApplyPullRequestOTAUpdate()
const [actyNotifNudged, setActyNotifNudged] = useActivitySubscriptionsNudged()
+ const growthbookControl = useDialogControl()
const resetOnboarding = () => {
navigation.navigate('Home')
@@ -458,6 +460,14 @@ function DevOptions() {
System log
+ growthbookControl.open()}
+ label={l`View GrowthBook information`}>
+
+ GrowthBook
+
+
+
navigation.navigate('Debug')}
label={l`Open storybook page`}>
@@ -474,7 +484,7 @@ function DevOptions() {
deleteChatDeclarationRecord()}
- label={l`Open storybook page`}>
+ label={l`Delete chat declaration record`}>
Delete chat declaration record
diff --git a/src/screens/Settings/components/GrowthbookDialog.tsx b/src/screens/Settings/components/GrowthbookDialog.tsx
new file mode 100644
index 0000000000..4ba8bfc582
--- /dev/null
+++ b/src/screens/Settings/components/GrowthbookDialog.tsx
@@ -0,0 +1,215 @@
+import {useState} from 'react'
+import {Pressable, View} from 'react-native'
+import * as Clipboard from 'expo-clipboard'
+
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import {atoms as a, native, useTheme} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
+import * as Dialog from '#/components/Dialog'
+import {ArrowRotateClockwise_Stroke2_Corner0_Rounded as ArrowRotate} from '#/components/icons/ArrowRotate'
+import {Loader} from '#/components/Loader'
+import * as ProfileCard from '#/components/ProfileCard'
+import * as Toast from '#/components/Toast'
+import {Text} from '#/components/Typography'
+import {Features, features, refresh} from '#/analytics/features'
+
+export function GrowthbookDialog({
+ control,
+}: {
+ control: Dialog.DialogControlProps
+}) {
+ return (
+
+
+
+
+ )
+}
+
+function GrowthbookDialogInner() {
+ const t = useTheme()
+
+ const [isRefreshing, setIsRefreshing] = useState(false)
+
+ return (
+
+ GrowthBook
+
+
+ }>
+
+
+
+
+
+
+
+ {Object.entries(Features).map(([name, key]) => (
+
+ ))}
+
+
+ )
+}
+
+function RefreshButton({
+ isRefreshing,
+ setIsRefreshing,
+}: {
+ isRefreshing: boolean
+ setIsRefreshing: React.Dispatch>
+}) {
+ const onPress = () => {
+ setIsRefreshing(true)
+ refresh({strategy: 'prefer-fresh-gates'})
+ .then(() => {
+ Toast.show('Refreshed feature flags', {type: 'success'})
+ })
+ .catch(() => {
+ Toast.show('Failed to refresh feature flags', {type: 'error'})
+ })
+ .finally(() => {
+ setIsRefreshing(false)
+ })
+ }
+
+ return (
+
+ )
+}
+
+function FeatureRow({
+ name,
+ featureKey,
+ isRefreshing,
+}: {
+ name: string
+ featureKey: string
+ isRefreshing: boolean
+}) {
+ const t = useTheme()
+ const value = features.evalFeature(featureKey).value
+
+ const onPress = () => {
+ void Clipboard.setStringAsync(featureKey)
+ Toast.show('Copied feature flag key to clipboard', {type: 'success'})
+ }
+
+ return (
+
+
+ {name}
+
+ {featureKey}
+
+
+ {isRefreshing ? : }
+
+ )
+}
+
+function CurrentProfile() {
+ const t = useTheme()
+ const {currentAccount} = useSession()
+ const moderationOpts = useModerationOpts()
+ const {data: profile} = useProfileQuery({did: currentAccount?.did})
+
+ if (!currentAccount) {
+ return (
+
+ No active account
+
+ )
+ }
+
+ const onPressDid = () => {
+ void Clipboard.setStringAsync(currentAccount.did)
+ Toast.show('Copied did to clipboard', {type: 'success'})
+ }
+
+ return profile && moderationOpts ? (
+
+
+
+
+
+
+ ) : null
+}
+
+function FeatureValue({value}: {value: unknown}) {
+ const t = useTheme()
+
+ let label: string
+ let color: string
+ if (value === true) {
+ label = 'true'
+ color = t.palette.positive_500
+ } else if (value === false) {
+ label = 'false'
+ color = t.palette.negative_500
+ } else if (value === null || value === undefined) {
+ label = 'null'
+ color = t.palette.contrast_500
+ } else {
+ label = JSON.stringify(value)
+ color = t.palette.contrast_700
+ }
+
+ return {label}
+}