mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {useCallback} from 'react'
2import {View} from 'react-native'
3import Animated, {
4 FadeIn,
5 FadeOut,
6 LayoutAnimationConfig,
7 LinearTransition,
8 StretchOutY,
9} from 'react-native-reanimated'
10import {type ComAtprotoServerListAppPasswords} from '@atproto/api'
11import {msg, Trans} from '@lingui/macro'
12import {useLingui} from '@lingui/react'
13import {type NativeStackScreenProps} from '@react-navigation/native-stack'
14
15import {type CommonNavigatorParams} from '#/lib/routes/types'
16import {cleanError} from '#/lib/strings/errors'
17import {isWeb} from '#/platform/detection'
18import {
19 useAppPasswordDeleteMutation,
20 useAppPasswordsQuery,
21} from '#/state/queries/app-passwords'
22import {EmptyState} from '#/view/com/util/EmptyState'
23import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
24import * as Toast from '#/view/com/util/Toast'
25import {atoms as a, useTheme} from '#/alf'
26import {Admonition, colors} from '#/components/Admonition'
27import {Button, ButtonIcon, ButtonText} from '#/components/Button'
28import {useDialogControl} from '#/components/Dialog'
29import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
30import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
31import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
32import * as Layout from '#/components/Layout'
33import {Loader} from '#/components/Loader'
34import * as Prompt from '#/components/Prompt'
35import {Text} from '#/components/Typography'
36import {AddAppPasswordDialog} from './components/AddAppPasswordDialog'
37import * as SettingsList from './components/SettingsList'
38
39type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
40export function AppPasswordsScreen({}: Props) {
41 const {_} = useLingui()
42 const {data: appPasswords, error} = useAppPasswordsQuery()
43 const createAppPasswordControl = useDialogControl()
44
45 return (
46 <Layout.Screen testID="AppPasswordsScreen">
47 <Layout.Header.Outer>
48 <Layout.Header.BackButton />
49 <Layout.Header.Content>
50 <Layout.Header.TitleText>
51 <Trans>App Passwords</Trans>
52 </Layout.Header.TitleText>
53 </Layout.Header.Content>
54 <Layout.Header.Slot />
55 </Layout.Header.Outer>
56 <Layout.Content>
57 {error ? (
58 <ErrorScreen
59 title={_(msg`Oops!`)}
60 message={_(msg`There was an issue fetching your app passwords`)}
61 details={cleanError(error)}
62 />
63 ) : (
64 <SettingsList.Container>
65 <SettingsList.Item>
66 <Admonition type="tip" style={[a.flex_1]}>
67 <Trans>
68 Use app passwords to sign in to other Bluesky clients without
69 giving full access to your account or password.
70 </Trans>
71 </Admonition>
72 </SettingsList.Item>
73 <SettingsList.Item>
74 <Button
75 label={_(msg`Add App Password`)}
76 size="large"
77 color="primary"
78 variant="solid"
79 onPress={() => createAppPasswordControl.open()}
80 style={[a.flex_1]}>
81 <ButtonIcon icon={PlusIcon} />
82 <ButtonText>
83 <Trans>Add App Password</Trans>
84 </ButtonText>
85 </Button>
86 </SettingsList.Item>
87 <SettingsList.Divider />
88 <LayoutAnimationConfig skipEntering skipExiting>
89 {appPasswords ? (
90 appPasswords.length > 0 ? (
91 <View style={[a.overflow_hidden]}>
92 {appPasswords.map(appPassword => (
93 <Animated.View
94 key={appPassword.name}
95 style={a.w_full}
96 entering={FadeIn}
97 exiting={isWeb ? FadeOut : StretchOutY}
98 layout={LinearTransition.delay(150)}>
99 <SettingsList.Item>
100 <AppPasswordCard appPassword={appPassword} />
101 </SettingsList.Item>
102 </Animated.View>
103 ))}
104 </View>
105 ) : (
106 <EmptyState
107 icon="growth"
108 message={_(msg`No app passwords yet`)}
109 />
110 )
111 ) : (
112 <View
113 style={[
114 a.flex_1,
115 a.justify_center,
116 a.align_center,
117 a.py_4xl,
118 ]}>
119 <Loader size="xl" />
120 </View>
121 )}
122 </LayoutAnimationConfig>
123 </SettingsList.Container>
124 )}
125 </Layout.Content>
126
127 <AddAppPasswordDialog
128 control={createAppPasswordControl}
129 passwords={appPasswords?.map(p => p.name) || []}
130 />
131 </Layout.Screen>
132 )
133}
134
135function AppPasswordCard({
136 appPassword,
137}: {
138 appPassword: ComAtprotoServerListAppPasswords.AppPassword
139}) {
140 const t = useTheme()
141 const {i18n, _} = useLingui()
142 const deleteControl = Prompt.usePromptControl()
143 const {mutateAsync: deleteMutation} = useAppPasswordDeleteMutation()
144
145 const onDelete = useCallback(async () => {
146 await deleteMutation({name: appPassword.name})
147 Toast.show(_(msg({message: 'App password deleted', context: 'toast'})))
148 }, [deleteMutation, appPassword.name, _])
149
150 return (
151 <View
152 style={[
153 a.w_full,
154 a.border,
155 a.rounded_sm,
156 a.px_md,
157 a.py_sm,
158 t.atoms.bg_contrast_25,
159 t.atoms.border_contrast_low,
160 ]}>
161 <View
162 style={[
163 a.flex_row,
164 a.justify_between,
165 a.align_start,
166 a.w_full,
167 a.gap_sm,
168 ]}>
169 <View style={[a.gap_xs]}>
170 <Text style={[t.atoms.text, a.text_md, a.font_bold]}>
171 {appPassword.name}
172 </Text>
173 <Text style={[t.atoms.text_contrast_medium]}>
174 <Trans>
175 Created{' '}
176 {i18n.date(appPassword.createdAt, {
177 year: 'numeric',
178 month: 'numeric',
179 day: 'numeric',
180 hour: '2-digit',
181 minute: '2-digit',
182 })}
183 </Trans>
184 </Text>
185 </View>
186 <Button
187 label={_(msg`Delete app password`)}
188 variant="ghost"
189 color="negative"
190 size="small"
191 style={[a.bg_transparent]}
192 onPress={() => deleteControl.open()}>
193 <ButtonIcon icon={TrashIcon} />
194 </Button>
195 </View>
196 {appPassword.privileged && (
197 <View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}>
198 <WarningIcon style={[{color: colors.warning[t.scheme]}]} />
199 <Text style={t.atoms.text_contrast_high}>
200 <Trans>Allows access to direct messages</Trans>
201 </Text>
202 </View>
203 )}
204
205 <Prompt.Basic
206 control={deleteControl}
207 title={_(msg`Delete app password?`)}
208 description={_(
209 msg`Are you sure you want to delete the app password "${appPassword.name}"?`,
210 )}
211 onConfirm={onDelete}
212 confirmButtonCta={_(msg`Delete`)}
213 confirmButtonColor="negative"
214 />
215 </View>
216 )
217}