219 lines
7.3 KiB
TypeScript
219 lines
7.3 KiB
TypeScript
import {useCallback} from 'react'
|
|
import {View} from 'react-native'
|
|
import Animated, {
|
|
FadeIn,
|
|
FadeOut,
|
|
LayoutAnimationConfig,
|
|
LinearTransition,
|
|
} from 'react-native-reanimated'
|
|
import {type ComAtprotoServerListAppPasswords} from '@atproto/api'
|
|
import {msg} from '@lingui/core/macro'
|
|
import {useLingui} from '@lingui/react'
|
|
import {Trans} from '@lingui/react/macro'
|
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
|
|
|
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
|
import {cleanError} from '#/lib/strings/errors'
|
|
import {
|
|
useAppPasswordDeleteMutation,
|
|
useAppPasswordsQuery,
|
|
} from '#/state/queries/app-passwords'
|
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
|
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
|
import {atoms as a, useTheme} from '#/alf'
|
|
import {Admonition} from '#/components/Admonition'
|
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
|
import {useDialogControl} from '#/components/Dialog'
|
|
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
|
|
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
|
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
|
import * as Layout from '#/components/Layout'
|
|
import {Loader} from '#/components/Loader'
|
|
import * as Prompt from '#/components/Prompt'
|
|
import * as Toast from '#/components/Toast'
|
|
import {Text} from '#/components/Typography'
|
|
import {AddAppPasswordDialog} from './components/AddAppPasswordDialog'
|
|
import * as SettingsList from './components/SettingsList'
|
|
|
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
|
|
export function AppPasswordsScreen({}: Props) {
|
|
const {_} = useLingui()
|
|
const {data: appPasswords, error} = useAppPasswordsQuery()
|
|
const createAppPasswordControl = useDialogControl()
|
|
|
|
return (
|
|
<Layout.Screen testID="AppPasswordsScreen">
|
|
<Layout.Header.Outer>
|
|
<Layout.Header.BackButton />
|
|
<Layout.Header.Content>
|
|
<Layout.Header.TitleText>
|
|
<Trans>App Passwords</Trans>
|
|
</Layout.Header.TitleText>
|
|
</Layout.Header.Content>
|
|
<Layout.Header.Slot />
|
|
</Layout.Header.Outer>
|
|
<Layout.Content>
|
|
{error ? (
|
|
<ErrorScreen
|
|
title={_(msg`Oops!`)}
|
|
message={_(msg`There was an issue fetching your app passwords`)}
|
|
details={cleanError(error)}
|
|
/>
|
|
) : (
|
|
<SettingsList.Container>
|
|
<SettingsList.Item>
|
|
<Admonition type="tip" style={[a.flex_1]}>
|
|
<Trans>
|
|
Use app passwords to sign in to other Bluesky clients without
|
|
giving full access to your account or password.
|
|
</Trans>
|
|
</Admonition>
|
|
</SettingsList.Item>
|
|
<SettingsList.Item>
|
|
<Button
|
|
label={_(msg`Add App Password`)}
|
|
size="large"
|
|
color="primary"
|
|
variant="solid"
|
|
onPress={() => createAppPasswordControl.open()}
|
|
style={[a.flex_1]}>
|
|
<ButtonIcon icon={PlusIcon} />
|
|
<ButtonText>
|
|
<Trans>Add App Password</Trans>
|
|
</ButtonText>
|
|
</Button>
|
|
</SettingsList.Item>
|
|
<SettingsList.Divider />
|
|
<LayoutAnimationConfig skipEntering skipExiting>
|
|
{appPasswords ? (
|
|
appPasswords.length > 0 ? (
|
|
<View style={[a.overflow_hidden]}>
|
|
{appPasswords.map(appPassword => (
|
|
<Animated.View
|
|
key={appPassword.name}
|
|
style={a.w_full}
|
|
entering={FadeIn}
|
|
exiting={FadeOut}
|
|
layout={LinearTransition.delay(150)}>
|
|
<SettingsList.Item>
|
|
<AppPasswordCard appPassword={appPassword} />
|
|
</SettingsList.Item>
|
|
</Animated.View>
|
|
))}
|
|
</View>
|
|
) : (
|
|
<EmptyState
|
|
icon={Growth}
|
|
message={_(msg`No app passwords yet`)}
|
|
/>
|
|
)
|
|
) : (
|
|
<View
|
|
style={[
|
|
a.flex_1,
|
|
a.justify_center,
|
|
a.align_center,
|
|
a.py_4xl,
|
|
]}>
|
|
<Loader size="xl" />
|
|
</View>
|
|
)}
|
|
</LayoutAnimationConfig>
|
|
</SettingsList.Container>
|
|
)}
|
|
</Layout.Content>
|
|
|
|
<AddAppPasswordDialog
|
|
control={createAppPasswordControl}
|
|
passwords={appPasswords?.map(p => p.name) || []}
|
|
/>
|
|
</Layout.Screen>
|
|
)
|
|
}
|
|
|
|
function AppPasswordCard({
|
|
appPassword,
|
|
}: {
|
|
appPassword: ComAtprotoServerListAppPasswords.AppPassword
|
|
}) {
|
|
const t = useTheme()
|
|
const {i18n, _} = useLingui()
|
|
const deleteControl = Prompt.usePromptControl()
|
|
const {mutateAsync: deleteMutation} = useAppPasswordDeleteMutation()
|
|
|
|
const onDelete = useCallback(async () => {
|
|
await deleteMutation({name: appPassword.name})
|
|
Toast.show(_(msg({message: 'App password deleted', context: 'toast'})))
|
|
}, [deleteMutation, appPassword.name, _])
|
|
|
|
return (
|
|
<View
|
|
style={[
|
|
a.w_full,
|
|
a.border,
|
|
a.rounded_sm,
|
|
a.px_md,
|
|
a.py_sm,
|
|
t.atoms.bg_contrast_25,
|
|
t.atoms.border_contrast_low,
|
|
]}>
|
|
<View
|
|
style={[
|
|
a.flex_row,
|
|
a.justify_between,
|
|
a.align_start,
|
|
a.w_full,
|
|
a.gap_sm,
|
|
]}>
|
|
<View style={[a.gap_xs]}>
|
|
<Text style={[t.atoms.text, a.text_md, a.font_semi_bold]}>
|
|
{appPassword.name}
|
|
</Text>
|
|
<Text style={[t.atoms.text_contrast_medium]}>
|
|
<Trans>
|
|
Created{' '}
|
|
{i18n.date(appPassword.createdAt, {
|
|
year: 'numeric',
|
|
month: 'numeric',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</Trans>
|
|
</Text>
|
|
</View>
|
|
<Button
|
|
label={_(msg`Delete app password`)}
|
|
variant="ghost"
|
|
color="negative"
|
|
size="small"
|
|
shape="square"
|
|
style={[a.bg_transparent]}
|
|
onPress={() => deleteControl.open()}>
|
|
<ButtonIcon icon={TrashIcon} />
|
|
</Button>
|
|
</View>
|
|
{appPassword.privileged && (
|
|
<View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}>
|
|
<WarningIcon style={[{color: t.palette.yellow}]} />
|
|
<Text style={t.atoms.text_contrast_high}>
|
|
<Trans>Allows access to direct messages</Trans>
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
<Prompt.Basic
|
|
control={deleteControl}
|
|
title={_(msg`Delete app password?`)}
|
|
description={_(
|
|
msg`Are you sure you want to delete the app password "${appPassword.name}"?`,
|
|
)}
|
|
onConfirm={onDelete}
|
|
confirmButtonCta={_(msg`Delete`)}
|
|
confirmButtonColor="negative"
|
|
/>
|
|
</View>
|
|
)
|
|
}
|