Add setting for opting in to beta features (#11123)

This commit is contained in:
DS Boyce
2026-07-15 06:06:54 -07:00
committed by GitHub
parent d7f40b7e7f
commit 3c5c11c002
20 changed files with 685 additions and 90 deletions
@@ -0,0 +1,159 @@
import {useState} from 'react'
import {View} from 'react-native'
import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro'
import {useMutation} from '@tanstack/react-query'
import {logger} from '#/logger'
import {Sentry} from '#/logger/sentry/lib'
import {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
const MAX_FEEDBACK_LENGTH = 300
export function BetaFeaturesFeedbackDialog({
control,
betaFeatureKeys,
}: {
control: Dialog.DialogControlProps
/**
* The keys of the beta feature gates that are currently active for this user,
* attached to the feedback report for context.
*/
betaFeatureKeys: string[]
}) {
return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle />
<BetaFeaturesFeedbackDialogInner betaFeatureKeys={betaFeatureKeys} />
<Dialog.Close />
</Dialog.Outer>
)
}
function BetaFeaturesFeedbackDialogInner({
betaFeatureKeys,
}: {
betaFeatureKeys: string[]
}) {
const t = useTheme()
const {t: l} = useLingui()
const control = Dialog.useDialogContext()
const [feedback, setFeedback] = useState('')
const {mutate: onSubmit, isPending} = useMutation({
mutationFn: () => {
/*
* `captureFeedback` is a no-op when the Sentry client is disabled (e.g. in
* dev, where `init` sets `enabled: false`). It returns synchronously
* without sending or throwing, so guard here to route to the error toast
* instead of falsely reporting success.
*/
const enabled = Sentry.getClient()?.getOptions().enabled ?? false
if (!enabled) {
throw new Error('Sentry is disabled; feedback was not sent')
}
Sentry.captureFeedback(
{
message: feedback.trim(),
},
{
captureContext: {
contexts: {
betaFeatures: {keys: betaFeatureKeys},
},
},
},
)
return Promise.resolve()
},
onSuccess: () => {
control.close(() => {
setFeedback('')
Toast.show(l`Thanks for your feedback!`)
})
},
onError: error => {
logger.error('Failed to send beta features feedback', {
safeMessage: error,
})
Toast.show(l`Something went wrong, please try again.`, {type: 'error'})
},
})
const remaining = MAX_FEEDBACK_LENGTH - feedback.length
const canSubmit = feedback.trim().length > 0 && !isPending
return (
<Dialog.ScrollableInner
label={l`Share feedback`}
style={web({maxWidth: 400})}>
<View style={[a.gap_lg]}>
<View style={[a.gap_xs]}>
<Text style={[a.text_2xl, a.font_semi_bold]}>
{l`Share feedback`}
</Text>
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
{l`We’d love to hear about your experience testing beta features!`}
</Text>
</View>
<View style={[a.gap_xs]}>
<Text
accessibilityLabel={plural(remaining, {
one: '# character remaining',
other: '# characters remaining',
})}
accessibilityHint=""
style={[
a.text_sm,
a.text_right,
t.atoms.text_contrast_medium,
{fontVariant: ['tabular-nums']},
]}>
{remaining}
</Text>
<Dialog.Input
multiline
autoFocus
numberOfLines={5}
label={l`Share your thoughts…`}
value={feedback}
maxLength={MAX_FEEDBACK_LENGTH}
onChangeText={value =>
setFeedback(value.slice(0, MAX_FEEDBACK_LENGTH))
}
/>
</View>
<View style={[a.gap_sm]}>
<Button
label={l`Submit`}
size="large"
color="primary"
disabled={!canSubmit}
onPress={() => onSubmit()}>
<ButtonText>
<Trans>Submit feedback</Trans>
</ButtonText>
</Button>
<Button
label={l`Cancel`}
size="large"
color="secondary"
onPress={() =>
control.close(() => {
setFeedback('')
})
}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
</View>
</View>
</Dialog.ScrollableInner>
)
}