Files
bsky-social-app/src/screens/Settings/BetaFeaturesSettings.tsx
T

234 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_INTERNAL, 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 || IS_INTERNAL)
return (
<Layout.Screen>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content>
<Layout.Header.TitleText>
<Trans>Beta features</Trans>
</Layout.Header.TitleText>
</Layout.Header.Content>
<Layout.Header.Slot />
</Layout.Header.Outer>
<Layout.Content>
<SettingsList.Container>
<Toggle.Item
name="enable_beta_features"
label={l`Enable beta features`}
value={isBetaUser}
disabled={isPending}
onChange={value => void onChange(value)}>
<SettingsList.Item>
<View style={[a.flex_1, a.gap_2xs]}>
<SettingsList.ItemText style={[a.font_semi_bold]}>
<Trans>Enable beta features</Trans>
</SettingsList.ItemText>
<Text
style={[
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
<Trans>
Get early access to experimental features were testing.
</Trans>
</Text>
</View>
<Toggle.Platform />
</SettingsList.Item>
</Toggle.Item>
<View style={[a.px_xl, a.gap_md]}>
<Admonition type="info">
{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',
})}
</Admonition>
<Button
disabled={!canSubmitFeedback}
label={l`Share feedback`}
size="small"
color="primary_subtle"
onPress={onPressShareFeedback}
style={[a.self_start, a.mt_xs, a.mb_xl]}>
<ButtonIcon icon={BubbleInfoIcon} />
<ButtonText>
<Trans>Share feedback</Trans>
</ButtonText>
</Button>
{betaFeatures.length < 1 ? (
<View style={[a.align_center, a.gap_xs, a.py_5xl]}>
<BeakerIcon
size="4xl"
fill={t.palette.contrast_100}
style={[a.mb_md]}
/>
<Text style={[a.text_md, a.font_semi_bold, a.leading_snug]}>
<Trans>No beta features at the moment.</Trans>
</Text>
<Text
style={[a.text_sm, t.atoms.text_contrast_high]}
emoji={false}>
<Trans>Check back later!</Trans>
</Text>
</View>
) : (
<>
<Text style={[a.text_md, a.font_semi_bold]}>
<Trans>Current beta features</Trans>
</Text>
<View style={[a.gap_sm]}>
{betaFeatures.map(feature => (
<View
key={feature.key}
style={[
a.p_md,
a.rounded_md,
a.border,
t.atoms.border_contrast_low,
]}>
<Text
style={[
a.text_md,
a.font_medium,
a.pb_2xs,
t.atoms.text_contrast_high,
]}>
{feature.name}
</Text>
<Text
style={[
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_high,
]}>
{feature.description}
</Text>
</View>
))}
</View>
</>
)}
</View>
</SettingsList.Container>
</Layout.Content>
<BetaFeaturesFeedbackDialog
control={feedbackControl}
betaFeatureKeys={betaFeatures.map(feature => feature.key)}
/>
</Layout.Screen>
)
}