[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,216 @@
import React, {useState} from 'react'
import {StyleSheet, TextInput, View, TouchableOpacity} from 'react-native'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {s} from 'lib/styles'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb} from 'platform/detection'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import Clipboard from '@react-native-clipboard/clipboard'
import * as Toast from '../util/Toast'
export const snapPoints = ['70%']
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',
]
export function Component({}: {}) {
const pal = usePalette('default')
const store = useStores()
const [name, setName] = useState(
shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
)
const [appPassword, setAppPassword] = useState<string>()
const [wasCopied, setWasCopied] = useState(false)
const onCopy = React.useCallback(() => {
if (appPassword) {
Clipboard.setString(appPassword)
Toast.show('Copied to clipboard')
setWasCopied(true)
}
}, [appPassword])
const onDone = React.useCallback(() => {
store.shell.closeModal()
}, [store])
const createAppPassword = async () => {
try {
const newPassword = await store.me.createAppPassword(name)
if (newPassword) {
setAppPassword(newPassword.password)
} else {
Toast.show('Failed to create app password.')
// TODO: better error handling (?)
}
} catch (e) {
Toast.show('Failed to create app password.')
store.log.error('Failed to create app password', {e})
}
}
return (
<View style={[styles.container, pal.view]} testID="addAppPasswordsModal">
<View>
{!appPassword ? (
<Text type="lg">
Please enter a unique name for this App Password. We have generated
a random name for you.
</Text>
) : (
<Text type="lg">
<Text type="lg-bold">Here is your app password.</Text> Use this to
sign into the other app along with your handle.
</Text>
)}
{!appPassword ? (
<View style={[pal.btn, styles.textInputWrapper]}>
<TextInput
style={[styles.input, pal.text]}
onChangeText={setName}
value={name}
placeholder="Enter a name for this App Password"
placeholderTextColor={pal.colors.textLight}
autoCorrect={false}
autoComplete="off"
autoCapitalize="none"
autoFocus={true}
selectTextOnFocus={true}
multiline={true} // need this to be true otherwise selectTextOnFocus doesn't work
numberOfLines={1} // hack for multiline so only one line shows (android)
scrollEnabled={false} // hack for multiline so only one line shows (ios)
blurOnSubmit={true} // hack for multiline so it submits
editable={!appPassword}
returnKeyType="done"
onEndEditing={createAppPassword}
/>
</View>
) : (
<TouchableOpacity
style={[pal.border, styles.passwordContainer, pal.btn]}
onPress={onCopy}>
<Text type="2xl-bold">{appPassword}</Text>
{wasCopied ? (
<Text style={[pal.textLight]}>Copied</Text>
) : (
<FontAwesomeIcon
icon={['far', 'clone']}
style={pal.text as FontAwesomeIconStyle}
size={18}
/>
)}
</TouchableOpacity>
)}
</View>
{appPassword ? (
<Text type="lg" style={[pal.textLight, s.mb10]}>
For security reasons, you won't be able to view this again. If you
lose this password, you'll need to generate a new one.
</Text>
) : null}
<View style={styles.btnContainer}>
<Button
type="primary"
label={!appPassword ? 'Create App Password' : 'Done'}
style={styles.btn}
labelStyle={styles.btnLabel}
onPress={!appPassword ? createAppPassword : onDone}
/>
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingBottom: isDesktopWeb ? 0 : 50,
marginHorizontal: 16,
},
textInputWrapper: {
borderRadius: 8,
flexDirection: 'row',
alignItems: 'center',
marginTop: 16,
marginBottom: 8,
},
input: {
flex: 1,
width: '100%',
paddingVertical: 10,
paddingHorizontal: 8,
marginTop: 6,
fontSize: 17,
letterSpacing: 0.25,
fontWeight: '400',
borderRadius: 10,
},
passwordContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 8,
paddingHorizontal: 16,
alignItems: 'center',
borderRadius: 10,
marginTop: 16,
marginBottom: 12,
},
btnContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 12,
},
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 32,
paddingHorizontal: 60,
paddingVertical: 14,
},
btnLabel: {
fontSize: 18,
},
groupContent: {
borderTopWidth: 1,
flexDirection: 'row',
alignItems: 'center',
},
})

View file

@ -17,6 +17,7 @@ import * as DeleteAccountModal from './DeleteAccount'
import * as ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
const DEFAULT_SNAPPOINTS = ['90%']
@ -81,6 +82,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
} else if (activeModal?.name === 'invite-codes') {
snapPoints = InviteCodesModal.snapPoints
element = <InviteCodesModal.Component />
} else if (activeModal?.name === 'add-app-password') {
snapPoints = AddAppPassword.snapPoints
element = <AddAppPassword.Component />
} else if (activeModal?.name === 'content-filtering-settings') {
snapPoints = ContentFilteringSettingsModal.snapPoints
element = <ContentFilteringSettingsModal.Component />

View file

@ -3,6 +3,7 @@ import {observer} from 'mobx-react-lite'
import {Animated, StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
import {CenteredView} from './Views'
import {UserAvatar} from './UserAvatar'
import {Text} from './text/Text'
import {useStores} from 'state/index'
@ -18,10 +19,12 @@ export const ViewHeader = observer(function ({
title,
canGoBack,
hideOnScroll,
showOnDesktop,
}: {
title: string
canGoBack?: boolean
hideOnScroll?: boolean
showOnDesktop?: boolean
}) {
const pal = usePalette('default')
const store = useStores()
@ -42,7 +45,10 @@ export const ViewHeader = observer(function ({
}, [track, store])
if (isDesktopWeb) {
return <></>
if (showOnDesktop) {
return <DesktopWebHeader title={title} />
}
return null
} else {
if (typeof canGoBack === 'undefined') {
canGoBack = navigation.canGoBack()
@ -76,6 +82,19 @@ export const ViewHeader = observer(function ({
}
})
function DesktopWebHeader({title}: {title: string}) {
const pal = usePalette('default')
return (
<CenteredView style={[styles.header, styles.desktopHeader, pal.border]}>
<View style={styles.titleContainer} pointerEvents="none">
<Text type="title-lg" style={[pal.text, styles.title]}>
{title}
</Text>
</View>
</CenteredView>
)
}
const Container = observer(
({
children,
@ -133,6 +152,10 @@ const styles = StyleSheet.create({
top: 0,
width: '100%',
},
desktopHeader: {
borderBottomWidth: 1,
paddingVertical: 12,
},
titleContainer: {
marginLeft: 'auto',