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} +}