import {useEffect, useState} from 'react' import {View} from 'react-native' import {Trans, useLingui} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type CommonNavigatorParams} from '#/lib/routes/types' import {logger} from '#/logger' import { usePreferencesQuery, useSetIsBetaUserMutation, } from '#/state/queries/preferences' import {useSession} from '#/state/session' import {BetaFeaturesFeedbackDialog} from '#/screens/Settings/components/BetaFeaturesFeedbackDialog' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import * as Toggle from '#/components/forms/Toggle' import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import * as Layout from '#/components/Layout' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {features} from '#/analytics' import {getTargetedFeatures} from '#/analytics/features' import {IS_WEB} from '#/env' import {account} from '#/storage' type Props = NativeStackScreenProps< CommonNavigatorParams, 'BetaFeaturesSettings' > export function BetaFeaturesSettingsScreen({}: Props) { const t = useTheme() const {t: l, i18n} = useLingui() const {data: preferences} = usePreferencesQuery() const {currentAccount} = useSession() const {mutateAsync: setIsBetaUser} = useSetIsBetaUserMutation() const isBetaUser = preferences?.bskyAppState?.isBetaUser ?? false const [isPending, setIsPending] = useState(false) const feedbackControl = Dialog.useDialogControl() /* * `getTargetedFeatures` reads the GrowthBook singleton synchronously, but * gates arrive asynchronously (from `init` on mount, or `refresh` after a * toggle). Hold the result in state and re-read whenever fresh gates land so * the list reflects the latest evaluation instead of a stale render. */ const [betaFeatures, setBetaFeatures] = useState(() => getTargetedFeatures(i18n), ) useEffect(() => { let cancelled = false void features.refresh({strategy: 'prefer-fresh-gates'}).then(() => { if (!cancelled) setBetaFeatures(getTargetedFeatures(i18n)) }) return () => { cancelled = true } }, [i18n]) const onChange = async (next: boolean) => { try { setIsPending(true) await setIsBetaUser(next) /* * Cache the new value so analytics can set the `isBetaUser` GrowthBook * attribute synchronously on the next boot, before beta-gated features * are evaluated. Scoped per account, since `isBetaUser` is * account-specific. */ if (currentAccount) { account.set([currentAccount.did, 'isBetaUser'], next) } } catch (e) { logger.error('Failed to toggle beta features', {safeMessage: e}) Toast.show(l`Something went wrong, please try again.`, {type: 'error'}) return } finally { setIsPending(false) } /* * The toggle already succeeded; re-evaluate feature gates against the new * attribute in-session as a best-effort follow-up. A failure here should * not surface as a toggle error. */ try { await features.refresh({strategy: 'prefer-fresh-gates'}) setBetaFeatures(getTargetedFeatures(i18n)) } catch {} } const onPressShareFeedback = () => { feedbackControl.open() } const canSubmitFeedback = !isPending && isBetaUser && betaFeatures.length > 0 return ( Beta features void onChange(value)}> Enable beta features Get early access to experimental features we’re testing. {IS_WEB ? l({ message: 'Beta features may be unstable. Some changes may require reloading the app.', context: 'web', }) : l({ message: 'Beta features may be unstable. Some changes may require restarting the app.', context: 'native', })} {betaFeatures.length < 1 ? ( No beta features at the moment. Check back later! ) : ( <> Current beta features {betaFeatures.map(feature => ( {feature.name} {feature.description} ))} )} feature.key)} /> ) }