[Settings] Ungate, and remove old settings (#6144)

* move export car dialog

* move disableemail2fadialog

* delete old settings screens

* fix type error

* Update Navigation.tsx

* Delete AccountDropdownBtn.tsx

* remove old change handle modal

* delete add app paswords

* forgot to actually delete the change handle modal
This commit is contained in:
Samuel Newman
2024-11-08 22:42:18 +00:00
committed by GitHub
parent e8a03058bb
commit 5da3f29498
22 changed files with 16 additions and 3768 deletions
-131
View File
@@ -1,13 +1,11 @@
import React from 'react'
import {Linking, View} from 'react-native'
import {useSafeAreaFrame} from 'react-native-safe-area-context'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {LABELS} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {IS_INTERNAL} from '#/lib/app-info'
import {getLabelingServiceTitle} from '#/lib/moderation'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {logger} from '#/logger'
@@ -18,11 +16,6 @@ import {
UsePreferencesQueryResponse,
usePreferencesSetAdultContentMutation,
} from '#/state/queries/preferences'
import {
useProfileQuery,
useProfileUpdateMutation,
} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities'
import {useSetMinimalShellMode} from '#/state/shell'
import {ViewHeader} from '#/view/com/util/ViewHeader'
@@ -469,131 +462,7 @@ export function ModerationScreenInner({
})}
</View>
)}
{!IS_INTERNAL && (
<>
<Text
style={[
a.text_md,
a.font_bold,
a.pt_2xl,
a.pb_md,
t.atoms.text_contrast_high,
]}>
<Trans>Logged-out visibility</Trans>
</Text>
<PwiOptOut />
</>
)}
<View style={{height: 200}} />
</ScrollView>
)
}
function PwiOptOut() {
const t = useTheme()
const {_} = useLingui()
const {currentAccount} = useSession()
const {data: profile} = useProfileQuery({did: currentAccount?.did})
const updateProfile = useProfileUpdateMutation()
const isOptedOut =
profile?.labels?.some(l => l.val === '!no-unauthenticated') || false
const canToggle = profile && !updateProfile.isPending
const onToggleOptOut = React.useCallback(() => {
if (!profile) {
return
}
let wasAdded = false
updateProfile.mutate({
profile,
updates: existing => {
// create labels attr if needed
existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels)
? existing.labels
: {
$type: 'com.atproto.label.defs#selfLabels',
values: [],
}
// toggle the label
const hasLabel = existing.labels.values.some(
l => l.val === '!no-unauthenticated',
)
if (hasLabel) {
wasAdded = false
existing.labels.values = existing.labels.values.filter(
l => l.val !== '!no-unauthenticated',
)
} else {
wasAdded = true
existing.labels.values.push({val: '!no-unauthenticated'})
}
// delete if no longer needed
if (existing.labels.values.length === 0) {
delete existing.labels
}
return existing
},
checkCommitted: res => {
const exists = !!res.data.labels?.some(
l => l.val === '!no-unauthenticated',
)
return exists === wasAdded
},
})
}, [updateProfile, profile])
return (
<View style={[a.pt_sm]}>
<View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}>
<Toggle.Item
disabled={!canToggle}
value={isOptedOut}
onChange={onToggleOptOut}
name="logged_out_visibility"
style={a.flex_1}
label={_(
msg`Discourage apps from showing my account to logged-out users`,
)}>
<Toggle.Switch />
<Toggle.LabelText style={[a.text_md, a.flex_1]}>
<Trans>
Discourage apps from showing my account to logged-out users
</Trans>
</Toggle.LabelText>
</Toggle.Item>
{updateProfile.isPending && <Loader />}
</View>
<View style={[a.pt_md, a.gap_md, {paddingLeft: 38}]}>
<Text style={[a.leading_snug, t.atoms.text_contrast_high]}>
<Trans>
Bluesky will not show your profile and posts to logged-out users.
Other apps may not honor this request. This does not make your
account private.
</Trans>
</Text>
<Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}>
<Trans>
Note: Bluesky is an open and public network. This setting only
limits the visibility of your content on the Bluesky app and
website, and other apps may not respect this setting. Your content
may still be shown to logged-out users by other apps and websites.
</Trans>
</Text>
<InlineLinkText
label={_(msg`Learn more about what is public on Bluesky.`)}
to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy">
<Trans>Learn more about what is public on Bluesky.</Trans>
</InlineLinkText>
</View>
</View>
)
}
+1 -1
View File
@@ -6,7 +6,6 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from '#/lib/routes/types'
import {useModalControls} from '#/state/modals'
import {useSession} from '#/state/session'
import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
import {atoms as a, useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
@@ -24,6 +23,7 @@ import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/ico
import * as Layout from '#/components/Layout'
import {ChangeHandleDialog} from './components/ChangeHandleDialog'
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
import {ExportCarDialog} from './components/ExportCarDialog'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
export function AccountSettingsScreen({}: Props) {
@@ -0,0 +1,201 @@
import React, {useState} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {isNative} from '#/platform/detection'
import {useAgent, useSession} from '#/state/session'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Loader} from '#/components/Loader'
import {P, Text} from '#/components/Typography'
enum Stages {
Email,
ConfirmCode,
}
export function DisableEmail2FADialog({
control,
}: {
control: Dialog.DialogOuterProps['control']
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
const agent = useAgent()
const [stage, setStage] = useState<Stages>(Stages.Email)
const [confirmationCode, setConfirmationCode] = useState<string>('')
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [error, setError] = useState<string>('')
const onSendEmail = async () => {
setError('')
setIsProcessing(true)
try {
await agent.com.atproto.server.requestEmailUpdate()
setStage(Stages.ConfirmCode)
} catch (e) {
setError(cleanError(String(e)))
} finally {
setIsProcessing(false)
}
}
const onConfirmDisable = async () => {
setError('')
setIsProcessing(true)
try {
if (currentAccount?.email) {
await agent.com.atproto.server.updateEmail({
email: currentAccount!.email,
token: confirmationCode.trim(),
emailAuthFactor: false,
})
await agent.resumeSession(agent.session!)
Toast.show(_(msg`Email 2FA disabled`))
}
control.close()
} catch (e) {
const errMsg = String(e)
if (errMsg.includes('Token is invalid')) {
setError(_(msg`Invalid 2FA confirmation code.`))
} else {
setError(cleanError(errMsg))
}
} finally {
setIsProcessing(false)
}
}
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<View style={[a.relative, a.gap_md, a.w_full]}>
<Text
nativeID="dialog-title"
style={[a.text_2xl, a.font_bold, t.atoms.text]}>
<Trans>Disable Email 2FA</Trans>
</Text>
<P nativeID="dialog-description">
{stage === Stages.ConfirmCode ? (
<Trans>
An email has been sent to{' '}
{currentAccount?.email || '(no email)'}. It includes a
confirmation code which you can enter below.
</Trans>
) : (
<Trans>
To disable the email 2FA method, please verify your access to
the email address.
</Trans>
)}
</P>
{error ? <ErrorMessage message={error} /> : undefined}
{stage === Stages.Email ? (
<View
style={[
a.gap_sm,
gtMobile && [a.flex_row, a.justify_end, a.gap_md],
]}>
<Button
testID="sendEmailButton"
variant="solid"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={onSendEmail}
label={_(msg`Send verification email`)}
disabled={isProcessing}>
<ButtonText>
<Trans>Send verification email</Trans>
</ButtonText>
{isProcessing && <ButtonIcon icon={Loader} />}
</Button>
<Button
testID="haveCodeButton"
variant="ghost"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={() => setStage(Stages.ConfirmCode)}
label={_(msg`I have a code`)}
disabled={isProcessing}>
<ButtonText>
<Trans>I have a code</Trans>
</ButtonText>
</Button>
</View>
) : stage === Stages.ConfirmCode ? (
<View>
<View style={[a.mb_md]}>
<TextField.LabelText>
<Trans>Confirmation code</Trans>
</TextField.LabelText>
<TextField.Root>
<TextField.Icon icon={Lock} />
<Dialog.Input
testID="confirmationCode"
label={_(msg`Confirmation code`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="off"
value={confirmationCode}
onChangeText={setConfirmationCode}
onSubmitEditing={onConfirmDisable}
editable={!isProcessing}
/>
</TextField.Root>
</View>
<View
style={[
a.gap_sm,
gtMobile && [a.flex_row, a.justify_end, a.gap_md],
]}>
<Button
testID="resendCodeBtn"
variant="ghost"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={onSendEmail}
label={_(msg`Resend email`)}
disabled={isProcessing}>
<ButtonText>
<Trans>Resend email</Trans>
</ButtonText>
</Button>
<Button
testID="confirmBtn"
variant="solid"
color="primary"
size={gtMobile ? 'small' : 'large'}
onPress={onConfirmDisable}
label={_(msg`Confirm`)}
disabled={isProcessing}>
<ButtonText>
<Trans>Confirm</Trans>
</ButtonText>
{isProcessing && <ButtonIcon icon={Loader} />}
</Button>
</View>
</View>
) : undefined}
{!gtMobile && isNative && <View style={{height: 40}} />}
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}
@@ -4,9 +4,9 @@ import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {useAgent, useSession} from '#/state/session'
import {DisableEmail2FADialog} from '#/view/screens/Settings/DisableEmail2FADialog'
import {useDialogControl} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
import {DisableEmail2FADialog} from './DisableEmail2FADialog'
import * as SettingsList from './SettingsList'
export function Email2FAToggle() {
@@ -0,0 +1,110 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {saveBytesToDisk} from '#/lib/media/manip'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download'
import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function ExportCarDialog({
control,
}: {
control: Dialog.DialogOuterProps['control']
}) {
const {_} = useLingui()
const t = useTheme()
const agent = useAgent()
const [loading, setLoading] = React.useState(false)
const download = React.useCallback(async () => {
if (!agent.session) {
return // shouldnt ever happen
}
try {
setLoading(true)
const did = agent.session.did
const downloadRes = await agent.com.atproto.sync.getRepo({did})
const saveRes = await saveBytesToDisk(
'repo.car',
downloadRes.data,
downloadRes.headers['content-type'],
)
if (saveRes) {
Toast.show(_(msg`File saved successfully!`))
}
} catch (e) {
logger.error('Error occurred while downloading CAR file', {message: e})
Toast.show(_(msg`Error occurred while saving file`), 'xmark')
} finally {
setLoading(false)
control.close()
}
}, [_, control, agent])
return (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
accessibilityLabelledBy="dialog-title">
<View style={[a.relative, a.gap_lg, a.w_full]}>
<Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}>
<Trans>Export My Data</Trans>
</Text>
<Text nativeID="dialog-description" style={[a.text_sm]}>
<Trans>
Your account repository, containing all public data records, can
be downloaded as a "CAR" file. This file does not include media
embeds, such as images, or your private data, which must be
fetched separately.
</Trans>
</Text>
<Button
variant="solid"
color="primary"
size="large"
label={_(msg`Download CAR file`)}
disabled={loading}
onPress={download}>
<ButtonIcon icon={DownloadIcon} />
<ButtonText>
<Trans>Download CAR file</Trans>
</ButtonText>
{loading && <ButtonIcon icon={Loader} />}
</Button>
<Text
style={[
t.atoms.text_contrast_medium,
a.text_sm,
a.leading_snug,
a.flex_1,
]}>
<Trans>
This feature is in beta. You can read more about repository
exports in{' '}
<InlineLinkText
label={_(msg`View blogpost for more details`)}
to="https://docs.bsky.app/blog/repo-export"
style={[a.text_sm]}>
this blogpost
</InlineLinkText>
.
</Trans>
</Text>
</View>
</Dialog.ScrollableInner>
</Dialog.Outer>
)
}