[APP-522] Create & revoke App Passwords within settings (#505)

* create and delete app passwords

* add randomly generated name

* Tweak copy and layout of app passwords

* Improve app passwords on desktop web

* Rearrange settings

* Change app-passwords route and add to backend

* Fix link

* Fix some more desktop web

* Remove log

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Ansh 2023-04-21 16:55:29 -07:00 committed by GitHub
parent aa56f4a5e2
commit 38eb299011
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 607 additions and 8 deletions

View file

@ -0,0 +1,275 @@
import React from 'react'
import {Alert, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {ScrollView} from 'react-native-gesture-handler'
import {Text} from '../com/util/text/Text'
import {Button} from '../com/util/forms/Button'
import * as Toast from '../com/util/Toast'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb} from 'platform/detection'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {observer} from 'mobx-react-lite'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types'
import {useAnalytics} from 'lib/analytics'
import {useFocusEffect} from '@react-navigation/native'
import {ViewHeader} from '../com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
export const AppPasswords = withAuthRequired(
observer(({}: Props) => {
const pal = usePalette('default')
const store = useStores()
const {screen} = useAnalytics()
useFocusEffect(
React.useCallback(() => {
screen('Settings')
store.shell.setMinimalShellMode(false)
}, [screen, store]),
)
const onAdd = React.useCallback(async () => {
store.shell.openModal({name: 'add-app-password'})
}, [store])
// no app passwords (empty) state
if (store.me.appPasswords.length === 0) {
return (
<CenteredView
style={[
styles.container,
isDesktopWeb && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<AppPasswordsHeader />
<View style={[styles.empty, pal.viewLight]}>
<Text type="lg" style={[pal.text, styles.emptyText]}>
You have not created any app passwords yet. You can create one by
pressing the button below.
</Text>
</View>
{!isDesktopWeb && <View style={styles.flex1} />}
<View
style={[
styles.btnContainer,
isDesktopWeb && styles.btnContainerDesktop,
]}>
<Button
testID="appPasswordBtn"
type="primary"
label="Add App Password"
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onAdd}
/>
</View>
</CenteredView>
)
}
// has app passwords
return (
<CenteredView
style={[
styles.container,
isDesktopWeb && styles.containerDesktop,
pal.view,
pal.border,
]}
testID="appPasswordsScreen">
<AppPasswordsHeader />
<ScrollView
style={[
styles.scrollContainer,
pal.border,
!isDesktopWeb && styles.flex1,
]}>
{store.me.appPasswords.map((password, i) => (
<AppPassword
key={password.name}
testID={`appPassword-${i}`}
name={password.name}
createdAt={password.createdAt}
/>
))}
{isDesktopWeb && (
<View style={[styles.btnContainer, styles.btnContainerDesktop]}>
<Button
testID="appPasswordBtn"
type="primary"
label="Add App Password"
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onAdd}
/>
</View>
)}
</ScrollView>
{!isDesktopWeb && (
<View style={styles.btnContainer}>
<Button
testID="appPasswordBtn"
type="primary"
label="Add App Password"
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={onAdd}
/>
</View>
)}
</CenteredView>
)
}),
)
function AppPasswordsHeader() {
const pal = usePalette('default')
return (
<>
<ViewHeader title="App Passwords" showOnDesktop />
<Text
type="sm"
style={[
styles.description,
pal.text,
isDesktopWeb && styles.descriptionDesktop,
]}>
These passwords can be used to log onto Bluesky in other apps without
giving them full access to your account or your password.
</Text>
</>
)
}
function AppPassword({
testID,
name,
createdAt,
}: {
testID: string
name: string
createdAt: string
}) {
const pal = usePalette('default')
const store = useStores()
const onDelete = React.useCallback(async () => {
Alert.alert(
'Delete App Password',
`Are you sure you want to delete the app password "${name}"?`,
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Delete',
style: 'destructive',
onPress: async () => {
await store.me.deleteAppPassword(name)
Toast.show('App password deleted')
},
},
],
)
}, [store, name])
return (
<TouchableOpacity
testID={testID}
style={[styles.item, pal.border]}
onPress={onDelete}>
<Text type="md-bold" style={pal.text}>
{name}
</Text>
<View style={styles.flex1} />
<Text type="md" style={[pal.text, styles.pr10]}>
{new Date(createdAt).toDateString()}
</Text>
<FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} />
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: isDesktopWeb ? 0 : 100,
},
containerDesktop: {
borderLeftWidth: 1,
borderRightWidth: 1,
},
title: {
textAlign: 'center',
marginTop: 12,
marginBottom: 12,
},
description: {
textAlign: 'center',
paddingHorizontal: 20,
marginBottom: 14,
},
descriptionDesktop: {
marginTop: 14,
},
scrollContainer: {
borderTopWidth: 1,
marginTop: 4,
marginBottom: 16,
},
flex1: {
flex: 1,
},
empty: {
paddingHorizontal: 20,
paddingVertical: 20,
borderRadius: 16,
marginHorizontal: 24,
marginTop: 10,
},
emptyText: {
textAlign: 'center',
},
item: {
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1,
paddingHorizontal: 20,
paddingVertical: 14,
},
pr10: {
marginRight: 10,
},
btnContainer: {
flexDirection: 'row',
justifyContent: 'center',
},
btnContainerDesktop: {
marginTop: 14,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
paddingHorizontal: 60,
paddingVertical: 14,
},
btnLabel: {
fontSize: 18,
},
trashIcon: {
color: 'red',
},
})

View file

@ -22,7 +22,7 @@ export const PostLikedByScreen = withAuthRequired(({route}: Props) => {
return (
<View>
<ViewHeader title="Liked by" />
<ViewHeader title="Liked by" showOnDesktop />
<PostLikedByComponent uri={uri} />
</View>
)

View file

@ -22,7 +22,7 @@ export const PostRepostedByScreen = withAuthRequired(({route}: Props) => {
return (
<View>
<ViewHeader title="Reposted by" />
<ViewHeader title="Reposted by" showOnDesktop />
<PostRepostedByComponent uri={uri} />
</View>
)

View file

@ -20,7 +20,7 @@ export const ProfileFollowersScreen = withAuthRequired(({route}: Props) => {
return (
<View>
<ViewHeader title="Followers" />
<ViewHeader title="Followers" showOnDesktop />
<ProfileFollowersComponent name={name} />
</View>
)

View file

@ -20,7 +20,7 @@ export const ProfileFollowsScreen = withAuthRequired(({route}: Props) => {
return (
<View>
<ViewHeader title="Following" />
<ViewHeader title="Following" showOnDesktop />
<ProfileFollowsComponent name={name} />
</View>
)

View file

@ -140,7 +140,7 @@ export const SettingsScreen = withAuthRequired(
return (
<View style={[s.hContentRegion]} testID="settingsScreen">
<ViewHeader title="Settings" />
<ViewHeader title="Settings" showOnDesktop />
<ScrollView style={s.hContentRegion} scrollIndicatorInsets={{right: 1}}>
<View style={styles.spacer20} />
<View style={[s.flexRow, styles.heading]}>
@ -267,6 +267,20 @@ export const SettingsScreen = withAuthRequired(
Content moderation
</Text>
</TouchableOpacity>
<Link
testID="appPasswordBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
href="/settings/app-passwords">
<View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon
icon="lock"
style={pal.text as FontAwesomeIconStyle}
/>
</View>
<Text type="lg" style={pal.text}>
App Passwords
</Text>
</Link>
<TouchableOpacity
testID="changeHandleBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}