mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {useEffect, useMemo, useState} from 'react'
2import {useWindowDimensions, View} from 'react-native'
3import Animated, {
4 FadeIn,
5 FadeOut,
6 LayoutAnimationConfig,
7 LinearTransition,
8 SlideInRight,
9 SlideOutLeft,
10} from 'react-native-reanimated'
11import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
12import {msg, Trans} from '@lingui/macro'
13import {useLingui} from '@lingui/react'
14import {useMutation} from '@tanstack/react-query'
15
16import {isWeb} from '#/platform/detection'
17import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
18import {atoms as a, native, useTheme} from '#/alf'
19import {Admonition} from '#/components/Admonition'
20import {Button, ButtonIcon, ButtonText} from '#/components/Button'
21import * as Dialog from '#/components/Dialog'
22import * as TextInput from '#/components/forms/TextField'
23import * as Toggle from '#/components/forms/Toggle'
24import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
25import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
26import {Text} from '#/components/Typography'
27import {CopyButton} from './CopyButton'
28
29export function AddAppPasswordDialog({
30 control,
31 passwords,
32}: {
33 control: Dialog.DialogControlProps
34 passwords: string[]
35}) {
36 const {height} = useWindowDimensions()
37 return (
38 <Dialog.Outer control={control} nativeOptions={{minHeight: height}}>
39 <Dialog.Handle />
40 <CreateDialogInner passwords={passwords} />
41 </Dialog.Outer>
42 )
43}
44
45function CreateDialogInner({passwords}: {passwords: string[]}) {
46 const control = Dialog.useDialogContext()
47 const t = useTheme()
48 const {_} = useLingui()
49 const autogeneratedName = useRandomName()
50 const [name, setName] = useState('')
51 const [privileged, setPrivileged] = useState(false)
52 const {
53 mutateAsync: actuallyCreateAppPassword,
54 error: apiError,
55 data,
56 } = useAppPasswordCreateMutation()
57
58 const regexFailError = useMemo(
59 () =>
60 new DisplayableError(
61 _(
62 msg`App password names can only contain letters, numbers, spaces, dashes, and underscores`,
63 ),
64 ),
65 [_],
66 )
67
68 const {
69 mutate: createAppPassword,
70 error: validationError,
71 isPending,
72 } = useMutation<
73 ComAtprotoServerCreateAppPassword.AppPassword,
74 Error | DisplayableError
75 >({
76 mutationFn: async () => {
77 const chosenName = name.trim() || autogeneratedName
78 if (chosenName.length < 4) {
79 throw new DisplayableError(
80 _(msg`App password names must be at least 4 characters long`),
81 )
82 }
83 if (passwords.find(p => p === chosenName)) {
84 throw new DisplayableError(_(msg`App password name must be unique`))
85 }
86 return await actuallyCreateAppPassword({name: chosenName, privileged})
87 },
88 })
89
90 const [hasBeenCopied, setHasBeenCopied] = useState(false)
91 useEffect(() => {
92 if (hasBeenCopied) {
93 const timeout = setTimeout(() => setHasBeenCopied(false), 100)
94 return () => clearTimeout(timeout)
95 }
96 }, [hasBeenCopied])
97
98 const error =
99 validationError || (!name.match(/^[a-zA-Z0-9-_ ]*$/) && regexFailError)
100
101 return (
102 <Dialog.ScrollableInner label={_(msg`Add app password`)}>
103 <View style={[native(a.pt_md)]}>
104 <LayoutAnimationConfig skipEntering skipExiting>
105 {!data ? (
106 <Animated.View
107 style={[a.gap_lg]}
108 exiting={native(SlideOutLeft)}
109 key={0}>
110 <Text style={[a.text_2xl, a.font_bold]}>
111 <Trans>Add App Password</Trans>
112 </Text>
113 <Text style={[a.text_md, a.leading_snug]}>
114 <Trans>
115 Please enter a unique name for this app password or use our
116 randomly generated one.
117 </Trans>
118 </Text>
119 <View>
120 <TextInput.Root isInvalid={!!error}>
121 <Dialog.Input
122 label={_(msg`App Password`)}
123 placeholder={autogeneratedName}
124 onChangeText={setName}
125 returnKeyType="done"
126 onSubmitEditing={() => createAppPassword()}
127 blurOnSubmit
128 autoCorrect={false}
129 autoComplete="off"
130 autoCapitalize="none"
131 autoFocus
132 />
133 </TextInput.Root>
134 </View>
135 {error instanceof DisplayableError && (
136 <Animated.View entering={FadeIn} exiting={FadeOut}>
137 <Admonition type="error">{error.message}</Admonition>
138 </Animated.View>
139 )}
140 <Animated.View
141 style={[a.gap_lg]}
142 layout={native(LinearTransition)}>
143 <Toggle.Item
144 name="privileged"
145 type="checkbox"
146 label={_(msg`Allow access to your direct messages`)}
147 value={privileged}
148 onChange={setPrivileged}
149 style={[a.flex_1]}>
150 <Toggle.Checkbox />
151 <Toggle.LabelText
152 style={[a.font_normal, a.text_md, a.leading_snug]}>
153 <Trans>Allow access to your direct messages</Trans>
154 </Toggle.LabelText>
155 </Toggle.Item>
156 <Button
157 label={_(msg`Next`)}
158 size="large"
159 variant="solid"
160 color="primary"
161 style={[a.flex_1]}
162 onPress={() => createAppPassword()}
163 disabled={isPending}>
164 <ButtonText>
165 <Trans>Next</Trans>
166 </ButtonText>
167 <ButtonIcon icon={ChevronRight} />
168 </Button>
169 {!!apiError ||
170 (error && !(error instanceof DisplayableError) && (
171 <Animated.View entering={FadeIn} exiting={FadeOut}>
172 <Admonition type="error">
173 <Trans>
174 Failed to create app password. Please try again.
175 </Trans>
176 </Admonition>
177 </Animated.View>
178 ))}
179 </Animated.View>
180 </Animated.View>
181 ) : (
182 <Animated.View
183 style={[a.gap_lg]}
184 entering={isWeb ? FadeIn.delay(200) : SlideInRight}
185 key={1}>
186 <Text style={[a.text_2xl, a.font_bold]}>
187 <Trans>Here is your app password!</Trans>
188 </Text>
189 <Text style={[a.text_md, a.leading_snug]}>
190 <Trans>
191 Use this to sign into the other app along with your handle.
192 </Trans>
193 </Text>
194 <CopyButton
195 value={data.password}
196 label={_(msg`Copy App Password`)}
197 size="large"
198 variant="solid"
199 color="secondary">
200 <ButtonText>{data.password}</ButtonText>
201 <ButtonIcon icon={CopyIcon} />
202 </CopyButton>
203 <Text
204 style={[
205 a.text_md,
206 a.leading_snug,
207 t.atoms.text_contrast_medium,
208 ]}>
209 <Trans>
210 For security reasons, you won't be able to view this again. If
211 you lose this app password, you'll need to generate a new one.
212 </Trans>
213 </Text>
214 <Button
215 label={_(msg`Done`)}
216 size="large"
217 variant="outline"
218 color="primary"
219 style={[a.flex_1]}
220 onPress={() => control.close()}>
221 <ButtonText>
222 <Trans>Done</Trans>
223 </ButtonText>
224 </Button>
225 </Animated.View>
226 )}
227 </LayoutAnimationConfig>
228 </View>
229 <Dialog.Close />
230 </Dialog.ScrollableInner>
231 )
232}
233
234class DisplayableError extends Error {
235 constructor(message: string) {
236 super(message)
237 this.name = 'DisplayableError'
238 }
239}
240
241function useRandomName() {
242 return useState(
243 () => shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
244 )[0]
245}
246
247const shadesOfBlue: string[] = [
248 'AliceBlue',
249 'Aqua',
250 'Aquamarine',
251 'Azure',
252 'BabyBlue',
253 'Blue',
254 'BlueViolet',
255 'CadetBlue',
256 'CornflowerBlue',
257 'Cyan',
258 'DarkBlue',
259 'DarkCyan',
260 'DarkSlateBlue',
261 'DeepSkyBlue',
262 'DodgerBlue',
263 'ElectricBlue',
264 'LightBlue',
265 'LightCyan',
266 'LightSkyBlue',
267 'LightSteelBlue',
268 'MediumAquaMarine',
269 'MediumBlue',
270 'MediumSlateBlue',
271 'MidnightBlue',
272 'Navy',
273 'PowderBlue',
274 'RoyalBlue',
275 'SkyBlue',
276 'SlateBlue',
277 'SteelBlue',
278 'Teal',
279 'Turquoise',
280]