Files
bsky-social-app/src/screens/Settings/components/AddAppPasswordDialog.tsx
T
Minseo Lee d805dec08f Fix "log in", "log out" to "sign in", "sign out" (#7739)
* log to sign

* logged to signed
2025-02-17 10:52:06 -06:00

281 lines
8.8 KiB
TypeScript

import {useEffect, useMemo, useState} from 'react'
import {useWindowDimensions, View} from 'react-native'
import Animated, {
FadeIn,
FadeOut,
LayoutAnimationConfig,
LinearTransition,
SlideInRight,
SlideOutLeft,
} from 'react-native-reanimated'
import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import {isWeb} from '#/platform/detection'
import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
import {atoms as a, native, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import * as TextInput from '#/components/forms/TextField'
import * as Toggle from '#/components/forms/Toggle'
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
import {Text} from '#/components/Typography'
import {CopyButton} from './CopyButton'
export function AddAppPasswordDialog({
control,
passwords,
}: {
control: Dialog.DialogControlProps
passwords: string[]
}) {
const {height} = useWindowDimensions()
return (
<Dialog.Outer control={control} nativeOptions={{minHeight: height}}>
<Dialog.Handle />
<CreateDialogInner passwords={passwords} />
</Dialog.Outer>
)
}
function CreateDialogInner({passwords}: {passwords: string[]}) {
const control = Dialog.useDialogContext()
const t = useTheme()
const {_} = useLingui()
const autogeneratedName = useRandomName()
const [name, setName] = useState('')
const [privileged, setPrivileged] = useState(false)
const {
mutateAsync: actuallyCreateAppPassword,
error: apiError,
data,
} = useAppPasswordCreateMutation()
const regexFailError = useMemo(
() =>
new DisplayableError(
_(
msg`App password names can only contain letters, numbers, spaces, dashes, and underscores`,
),
),
[_],
)
const {
mutate: createAppPassword,
error: validationError,
isPending,
} = useMutation<
ComAtprotoServerCreateAppPassword.AppPassword,
Error | DisplayableError
>({
mutationFn: async () => {
const chosenName = name.trim() || autogeneratedName
if (chosenName.length < 4) {
throw new DisplayableError(
_(msg`App password names must be at least 4 characters long`),
)
}
if (passwords.find(p => p === chosenName)) {
throw new DisplayableError(_(msg`App password name must be unique`))
}
return await actuallyCreateAppPassword({name: chosenName, privileged})
},
})
const [hasBeenCopied, setHasBeenCopied] = useState(false)
useEffect(() => {
if (hasBeenCopied) {
const timeout = setTimeout(() => setHasBeenCopied(false), 100)
return () => clearTimeout(timeout)
}
}, [hasBeenCopied])
const error =
validationError || (!name.match(/^[a-zA-Z0-9-_ ]*$/) && regexFailError)
return (
<Dialog.ScrollableInner label={_(msg`Add app password`)}>
<View style={[native(a.pt_md)]}>
<LayoutAnimationConfig skipEntering skipExiting>
{!data ? (
<Animated.View
style={[a.gap_lg]}
exiting={native(SlideOutLeft)}
key={0}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Add App Password</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Please enter a unique name for this app password or use our
randomly generated one.
</Trans>
</Text>
<View>
<TextInput.Root isInvalid={!!error}>
<Dialog.Input
label={_(msg`App Password`)}
placeholder={autogeneratedName}
onChangeText={setName}
returnKeyType="done"
onSubmitEditing={() => createAppPassword()}
blurOnSubmit
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
autoFocus
/>
</TextInput.Root>
</View>
{error instanceof DisplayableError && (
<Animated.View entering={FadeIn} exiting={FadeOut}>
<Admonition type="error">{error.message}</Admonition>
</Animated.View>
)}
<Animated.View
style={[a.gap_lg]}
layout={native(LinearTransition)}>
<Toggle.Item
name="privileged"
type="checkbox"
label={_(msg`Allow access to your direct messages`)}
value={privileged}
onChange={setPrivileged}
style={[a.flex_1]}>
<Toggle.Checkbox />
<Toggle.LabelText
style={[a.font_normal, a.text_md, a.leading_snug]}>
<Trans>Allow access to your direct messages</Trans>
</Toggle.LabelText>
</Toggle.Item>
<Button
label={_(msg`Next`)}
size="large"
variant="solid"
color="primary"
style={[a.flex_1]}
onPress={() => createAppPassword()}
disabled={isPending}>
<ButtonText>
<Trans>Next</Trans>
</ButtonText>
<ButtonIcon icon={ChevronRight} />
</Button>
{!!apiError ||
(error && !(error instanceof DisplayableError) && (
<Animated.View entering={FadeIn} exiting={FadeOut}>
<Admonition type="error">
<Trans>
Failed to create app password. Please try again.
</Trans>
</Admonition>
</Animated.View>
))}
</Animated.View>
</Animated.View>
) : (
<Animated.View
style={[a.gap_lg]}
entering={isWeb ? FadeIn.delay(200) : SlideInRight}
key={1}>
<Text style={[a.text_2xl, a.font_bold]}>
<Trans>Here is your app password!</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Use this to sign in to the other app along with your handle.
</Trans>
</Text>
<CopyButton
value={data.password}
label={_(msg`Copy App Password`)}
size="large"
variant="solid"
color="secondary">
<ButtonText>{data.password}</ButtonText>
<ButtonIcon icon={CopyIcon} />
</CopyButton>
<Text
style={[
a.text_md,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
<Trans>
For security reasons, you won't be able to view this again. If
you lose this app password, you'll need to generate a new one.
</Trans>
</Text>
<Button
label={_(msg`Done`)}
size="large"
variant="outline"
color="primary"
style={[a.flex_1]}
onPress={() => control.close()}>
<ButtonText>
<Trans>Done</Trans>
</ButtonText>
</Button>
</Animated.View>
)}
</LayoutAnimationConfig>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}
class DisplayableError extends Error {
constructor(message: string) {
super(message)
this.name = 'DisplayableError'
}
}
function useRandomName() {
return useState(
() => shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
)[0]
}
const shadesOfBlue: string[] = [
'AliceBlue',
'Aqua',
'Aquamarine',
'Azure',
'BabyBlue',
'Blue',
'BlueViolet',
'CadetBlue',
'CornflowerBlue',
'Cyan',
'DarkBlue',
'DarkCyan',
'DarkSlateBlue',
'DeepSkyBlue',
'DodgerBlue',
'ElectricBlue',
'LightBlue',
'LightCyan',
'LightSkyBlue',
'LightSteelBlue',
'MediumAquaMarine',
'MediumBlue',
'MediumSlateBlue',
'MidnightBlue',
'Navy',
'PowderBlue',
'RoyalBlue',
'SkyBlue',
'SlateBlue',
'SteelBlue',
'Teal',
'Turquoise',
]