feat: mobile app package (#1075)
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
const PADDING = 14
|
||||
|
||||
export const RegistrationDescription = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitForegroundColor};
|
||||
font-size: ${({ theme }) => theme.mainTextFontSize}px;
|
||||
padding-left: ${PADDING}px;
|
||||
padding-right: ${PADDING}px;
|
||||
margin-bottom: ${PADDING}px;
|
||||
`
|
||||
|
||||
export const RegistrationInput = styled.TextInput.attrs(({ theme }) => ({
|
||||
underlineColorAndroid: 'transparent',
|
||||
placeholderTextColor: theme.stylekitNeutralColor,
|
||||
}))`
|
||||
font-size: ${({ theme }) => theme.mainTextFontSize}px;
|
||||
padding: 0px;
|
||||
color: ${({ theme }) => theme.stylekitForegroundColor};
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
export const RegularView = styled.View``
|
||||
254
packages/mobile/src/Screens/Settings/Sections/AuthSection.tsx
Normal file
254
packages/mobile/src/Screens/Settings/Sections/AuthSection.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { ButtonCell } from '@Root/Components/ButtonCell'
|
||||
import { SectionedAccessoryTableCell } from '@Root/Components/SectionedAccessoryTableCell'
|
||||
import { SectionedTableCell } from '@Root/Components/SectionedTableCell'
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { ThemeServiceContext } from '@Style/ThemeService'
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { Keyboard } from 'react-native'
|
||||
import { RegistrationDescription, RegistrationInput, RegularView } from './AuthSection.styled'
|
||||
|
||||
const DEFAULT_SIGN_IN_TEXT = 'Sign In'
|
||||
const DEFAULT_REGISTER_TEXT = 'Register'
|
||||
const SIGNIN_IN = 'Generating Keys...'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
signedIn: boolean
|
||||
}
|
||||
|
||||
export const AuthSection = (props: Props) => {
|
||||
// Context
|
||||
const application = useContext(ApplicationContext)
|
||||
const themeService = useContext(ThemeServiceContext)
|
||||
|
||||
// State
|
||||
const [registering, setRegistering] = useState(false)
|
||||
const [signingIn, setSigningIn] = useState(false)
|
||||
const [strictSignIn, setStrictSignIn] = useState(false)
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [server, setServer] = useState('')
|
||||
const [passwordConfirmation, setPasswordConfirmation] = useState('')
|
||||
const [confirmRegistration, setConfirmRegistration] = useState(false)
|
||||
|
||||
// set initial server
|
||||
useEffect(() => {
|
||||
const getServer = async () => {
|
||||
const host = await application?.getHost()
|
||||
setServer(host!)
|
||||
}
|
||||
void getServer()
|
||||
}, [application])
|
||||
|
||||
const updateServer = useCallback(
|
||||
async (host: string) => {
|
||||
setServer(host)
|
||||
await application?.setCustomHost(host)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
if (props.signedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
if (!email) {
|
||||
void application?.alertService?.alert('Please enter a valid email address.', 'Missing Email', 'OK')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
void application?.alertService?.alert('Please enter your password.', 'Missing Password', 'OK')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const signIn = async () => {
|
||||
setSigningIn(true)
|
||||
if (!validate()) {
|
||||
setSigningIn(false)
|
||||
return
|
||||
}
|
||||
Keyboard.dismiss()
|
||||
const result = await application!.signIn(email, password, strictSignIn, undefined, true, false)
|
||||
|
||||
if (result?.error) {
|
||||
if (result?.error.message) {
|
||||
void application?.alertService?.alert(result?.error.message)
|
||||
}
|
||||
setSigningIn(false)
|
||||
return
|
||||
}
|
||||
|
||||
setSigningIn(false)
|
||||
setPassword('')
|
||||
setPasswordConfirmation('')
|
||||
}
|
||||
|
||||
const onRegisterPress = () => {
|
||||
if (!validate()) {
|
||||
return
|
||||
}
|
||||
setConfirmRegistration(true)
|
||||
}
|
||||
|
||||
const register = async () => {
|
||||
setRegistering(true)
|
||||
if (password !== passwordConfirmation) {
|
||||
void application?.alertService?.alert(
|
||||
'The passwords you entered do not match. Please try again.',
|
||||
"Passwords Don't Match",
|
||||
'OK',
|
||||
)
|
||||
} else {
|
||||
try {
|
||||
Keyboard.dismiss()
|
||||
await application!.register(email, password, undefined, true)
|
||||
} catch (error) {
|
||||
void application?.alertService?.alert((error as Error).message)
|
||||
}
|
||||
}
|
||||
setRegistering(false)
|
||||
}
|
||||
|
||||
const _renderRegistrationConfirm = () => {
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={'Confirm Password'} />
|
||||
|
||||
<RegistrationDescription>
|
||||
Due to the nature of our encryption, Standard Notes cannot offer password reset functionality. If you forget
|
||||
your password, you will permanently lose access to your data.
|
||||
</RegistrationDescription>
|
||||
|
||||
<SectionedTableCell first textInputCell>
|
||||
<RegistrationInput
|
||||
testID="passwordConfirmationField"
|
||||
placeholder={'Password confirmation'}
|
||||
onChangeText={setPasswordConfirmation}
|
||||
value={passwordConfirmation}
|
||||
secureTextEntry
|
||||
autoFocus
|
||||
keyboardAppearance={themeService?.keyboardColorForActiveTheme()}
|
||||
/>
|
||||
</SectionedTableCell>
|
||||
|
||||
<ButtonCell
|
||||
testID="registerConfirmButton"
|
||||
disabled={registering}
|
||||
title={registering ? 'Generating Keys...' : 'Register'}
|
||||
bold
|
||||
onPress={register}
|
||||
/>
|
||||
|
||||
<ButtonCell
|
||||
title="Cancel"
|
||||
onPress={() => {
|
||||
setConfirmRegistration(false)
|
||||
setPasswordConfirmation('')
|
||||
setPassword('')
|
||||
}}
|
||||
/>
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
|
||||
const _renderDefaultContent = () => {
|
||||
const keyboardApperance = themeService?.keyboardColorForActiveTheme()
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
{props.title && <SectionHeader title={props.title} />}
|
||||
|
||||
<>
|
||||
<RegularView>
|
||||
<SectionedTableCell textInputCell first>
|
||||
<RegistrationInput
|
||||
testID="emailField"
|
||||
placeholder={'Email'}
|
||||
onChangeText={setEmail}
|
||||
value={email ?? undefined}
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
keyboardType={'email-address'}
|
||||
textContentType={'emailAddress'}
|
||||
keyboardAppearance={keyboardApperance}
|
||||
/>
|
||||
</SectionedTableCell>
|
||||
|
||||
<SectionedTableCell textInputCell>
|
||||
<RegistrationInput
|
||||
testID="passwordField"
|
||||
placeholder={'Password'}
|
||||
onChangeText={setPassword}
|
||||
value={password ?? undefined}
|
||||
textContentType={'password'}
|
||||
secureTextEntry
|
||||
keyboardAppearance={keyboardApperance}
|
||||
/>
|
||||
</SectionedTableCell>
|
||||
</RegularView>
|
||||
|
||||
{(showAdvanced || !server) && (
|
||||
<RegularView>
|
||||
<SectionHeader title={'Advanced'} />
|
||||
<SectionedTableCell textInputCell first>
|
||||
<RegistrationInput
|
||||
testID="syncServerField"
|
||||
placeholder={'Sync Server'}
|
||||
onChangeText={updateServer}
|
||||
value={server}
|
||||
autoCorrect={false}
|
||||
autoCapitalize={'none'}
|
||||
keyboardType={'url'}
|
||||
keyboardAppearance={keyboardApperance}
|
||||
/>
|
||||
</SectionedTableCell>
|
||||
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={() => setStrictSignIn(!strictSignIn)}
|
||||
text={'Use strict sign in'}
|
||||
selected={() => {
|
||||
return strictSignIn
|
||||
}}
|
||||
/>
|
||||
</RegularView>
|
||||
)}
|
||||
</>
|
||||
|
||||
<ButtonCell
|
||||
testID="signInButton"
|
||||
title={signingIn ? SIGNIN_IN : DEFAULT_SIGN_IN_TEXT}
|
||||
disabled={signingIn}
|
||||
bold={true}
|
||||
onPress={signIn}
|
||||
/>
|
||||
|
||||
<ButtonCell
|
||||
testID="registerButton"
|
||||
title={DEFAULT_REGISTER_TEXT}
|
||||
disabled={registering}
|
||||
bold
|
||||
onPress={onRegisterPress}
|
||||
/>
|
||||
|
||||
{!showAdvanced && (
|
||||
<ButtonCell testID="advancedOptionsButton" title="Advanced Options" onPress={() => setShowAdvanced(true)} />
|
||||
)}
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<RegularView>
|
||||
{confirmRegistration && _renderRegistrationConfirm()}
|
||||
|
||||
{!confirmRegistration && _renderDefaultContent()}
|
||||
</RegularView>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const Label = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitNeutralColor};
|
||||
margin-top: 3px;
|
||||
`
|
||||
export const ContentContainer = styled.View`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ApplicationState } from '@Lib/ApplicationState'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { ButtonCell } from '@Root/Components/ButtonCell'
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import React, { useContext } from 'react'
|
||||
import { Platform, Share } from 'react-native'
|
||||
import { ContentContainer, Label } from './CompanySection.styled'
|
||||
|
||||
const URLS = {
|
||||
feedback: `mailto:help@standardnotes.com?subject=${Platform.OS === 'android' ? 'Android' : 'iOS'} app feedback (v${
|
||||
ApplicationState.version
|
||||
})`,
|
||||
learn_more: 'https://standardnotes.com',
|
||||
privacy: 'https://standardnotes.com/privacy',
|
||||
help: 'https://standardnotes.com/help',
|
||||
rate: Platform.select({
|
||||
ios: 'https://itunes.apple.com/us/app/standard-notes/id1285392450?ls=1&mt=8',
|
||||
android: 'market://details?id=com.standardnotes',
|
||||
}) as string,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
}
|
||||
|
||||
export const CompanySection = (props: Props) => {
|
||||
const application = useContext(ApplicationContext)
|
||||
const storeName = Platform.OS === 'android' ? 'Play Store' : 'App Store'
|
||||
|
||||
const openUrl = (action: keyof typeof URLS) => {
|
||||
application?.deviceInterface!.openUrl(URLS[action])
|
||||
}
|
||||
|
||||
const shareEncryption = () => {
|
||||
const title = 'The Unexpected Benefits of Encrypted Writing'
|
||||
let message = Platform.OS === 'ios' ? title : ''
|
||||
const url = 'https://standardnotes.com/why-encrypted'
|
||||
// Android ignores url. iOS ignores title.
|
||||
if (Platform.OS === 'android') {
|
||||
message += '\n\nhttps://standardnotes.com/why-encrypted'
|
||||
}
|
||||
void application?.getAppState().performActionWithoutStateChangeImpact(() => {
|
||||
void Share.share({ title: title, message: message, url: url })
|
||||
})
|
||||
}
|
||||
|
||||
const shareWithFriend = () => {
|
||||
const title = 'Standard Notes'
|
||||
let message = 'Check out Standard Notes, a free, open-source, and completely encrypted notes app.'
|
||||
const url = 'https://standardnotes.com'
|
||||
// Android ignores url. iOS ignores title.
|
||||
if (Platform.OS === 'android') {
|
||||
message += '\n\nhttps://standardnotes.com'
|
||||
}
|
||||
void application?.getAppState().performActionWithoutStateChangeImpact(() => {
|
||||
void Share.share({ title: title, message: message, url: url })
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={props.title} />
|
||||
|
||||
<ButtonCell first leftAligned={true} title="Help" onPress={() => openUrl('help')}>
|
||||
<Label>https://standardnotes.com/help</Label>
|
||||
</ButtonCell>
|
||||
<ButtonCell leftAligned={true} title="Contact Support" onPress={() => openUrl('feedback')}>
|
||||
<ContentContainer>
|
||||
<Label>help@standardnotes.com</Label>
|
||||
</ContentContainer>
|
||||
</ButtonCell>
|
||||
|
||||
<ButtonCell leftAligned={true} title="Spread Encryption" onPress={shareEncryption}>
|
||||
<Label>Share the unexpected benefits of encrypted writing.</Label>
|
||||
</ButtonCell>
|
||||
|
||||
<ButtonCell leftAligned={true} title="Tell a Friend" onPress={shareWithFriend}>
|
||||
<Label>Share Standard Notes with a friend.</Label>
|
||||
</ButtonCell>
|
||||
|
||||
<ButtonCell leftAligned={true} title="Learn About Standard Notes" onPress={() => openUrl('learn_more')}>
|
||||
<Label>https://standardnotes.com</Label>
|
||||
</ButtonCell>
|
||||
|
||||
<ButtonCell leftAligned={true} title="Our Privacy Manifesto" onPress={() => openUrl('privacy')}>
|
||||
<Label>https://standardnotes.com/privacy</Label>
|
||||
</ButtonCell>
|
||||
|
||||
<ButtonCell leftAligned={true} title="Rate Standard Notes" onPress={() => openUrl('rate')}>
|
||||
<ContentContainer>
|
||||
<Label>Version {ApplicationState.version}</Label>
|
||||
<Label>Help support us with a review on the {storeName}.</Label>
|
||||
</ContentContainer>
|
||||
</ButtonCell>
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { SectionedTableCell } from '@Root/Components/SectionedTableCell'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const BaseView = styled.View``
|
||||
|
||||
export const StyledSectionedTableCell = styled(SectionedTableCell)`
|
||||
padding-top: 12px;
|
||||
`
|
||||
|
||||
export const Title = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitForegroundColor};
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
`
|
||||
|
||||
export const Subtitle = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitNeutralColor};
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
`
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useIsLocked } from '@Lib/SnjsHelperHooks'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { ContentType, StorageEncryptionPolicy } from '@standardnotes/snjs'
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { BaseView, StyledSectionedTableCell, Subtitle, Title } from './EncryptionSection.styled'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
encryptionAvailable: boolean
|
||||
}
|
||||
|
||||
export const EncryptionSection = (props: Props) => {
|
||||
// Context
|
||||
const application = useContext(ApplicationContext)
|
||||
const [isLocked] = useIsLocked()
|
||||
|
||||
// State
|
||||
const [protocolDisplayName, setProtocolDisplayName] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.encryptionAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
let mounted = true
|
||||
const getProtocolDisplayName = async () => {
|
||||
const displayName = (await application?.getProtocolEncryptionDisplayName()) ?? ''
|
||||
if (mounted) {
|
||||
setProtocolDisplayName(displayName)
|
||||
}
|
||||
}
|
||||
void getProtocolDisplayName()
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [application, props.encryptionAvailable])
|
||||
|
||||
const textData = useMemo(() => {
|
||||
const encryptionType = protocolDisplayName
|
||||
let encryptionStatus = props.encryptionAvailable ? 'Enabled' : 'Not Enabled'
|
||||
if (props.encryptionAvailable) {
|
||||
encryptionStatus += ` | ${encryptionType}`
|
||||
} else {
|
||||
encryptionStatus += '. '
|
||||
encryptionStatus +=
|
||||
application?.getStorageEncryptionPolicy() === StorageEncryptionPolicy.Default
|
||||
? 'To enable encryption, sign in, register, or enable storage encryption.'
|
||||
: 'Sign in, register, or add a local passcode to enable encryption.'
|
||||
}
|
||||
let sourceString
|
||||
if (isLocked) {
|
||||
return { title: '', text: '' }
|
||||
} else {
|
||||
sourceString = application?.hasAccount() ? 'Account Keys' : 'Passcode'
|
||||
}
|
||||
|
||||
const items = application!.items.getItems([ContentType.Note, ContentType.Tag])
|
||||
const itemsStatus = items.length + '/' + items.length + ' notes and tags encrypted'
|
||||
|
||||
return {
|
||||
encryptionStatus,
|
||||
sourceString,
|
||||
itemsStatus,
|
||||
}
|
||||
}, [application, props.encryptionAvailable, isLocked, protocolDisplayName])
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={props.title} />
|
||||
|
||||
<StyledSectionedTableCell last={!props.encryptionAvailable} first={true}>
|
||||
<BaseView>
|
||||
<Title>Encryption</Title>
|
||||
<Subtitle>{textData.encryptionStatus}</Subtitle>
|
||||
</BaseView>
|
||||
</StyledSectionedTableCell>
|
||||
|
||||
{props.encryptionAvailable && (
|
||||
<StyledSectionedTableCell>
|
||||
<BaseView>
|
||||
<Title>Encryption Source</Title>
|
||||
<Subtitle>{textData.sourceString}</Subtitle>
|
||||
</BaseView>
|
||||
</StyledSectionedTableCell>
|
||||
)}
|
||||
|
||||
{props.encryptionAvailable && (
|
||||
<StyledSectionedTableCell last>
|
||||
<BaseView>
|
||||
<Title>Items Encrypted</Title>
|
||||
<Subtitle>{textData.itemsStatus}</Subtitle>
|
||||
</BaseView>
|
||||
</StyledSectionedTableCell>
|
||||
)}
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { SectionedTableCell } from '@Components/SectionedTableCell'
|
||||
import { StyleSheet } from 'react-native'
|
||||
import styled, { DefaultTheme } from 'styled-components/native'
|
||||
|
||||
export const useFilesInPreferencesStyles = (theme: DefaultTheme) => {
|
||||
return StyleSheet.create({
|
||||
progressBarContainer: {
|
||||
backgroundColor: theme.stylekitSecondaryContrastBackgroundColor,
|
||||
height: 8,
|
||||
borderRadius: 8,
|
||||
marginTop: 6,
|
||||
},
|
||||
progressBar: {
|
||||
backgroundColor: theme.stylekitInfoColor,
|
||||
borderRadius: 8,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const StyledSectionedTableCell = styled(SectionedTableCell)`
|
||||
padding-top: 12px;
|
||||
`
|
||||
export const Title = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitForegroundColor};
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
`
|
||||
export const SubTitle = styled.Text`
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: ${({ theme }) => theme.stylekitContrastForegroundColor};
|
||||
opacity: 0.6;
|
||||
`
|
||||
@@ -0,0 +1,56 @@
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import {
|
||||
StyledSectionedTableCell,
|
||||
SubTitle,
|
||||
Title,
|
||||
useFilesInPreferencesStyles,
|
||||
} from '@Screens/Settings/Sections/FilesSection.styled'
|
||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||
import { SubscriptionSettingName } from '@standardnotes/snjs'
|
||||
import React, { FC, useContext, useEffect, useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
export const FilesSection: FC = () => {
|
||||
const application = useSafeApplicationContext()
|
||||
const theme = useContext(ThemeContext)
|
||||
const styles = useFilesInPreferencesStyles(theme)
|
||||
|
||||
const [filesUsedQuota, setFilesUsedQuota] = useState(0)
|
||||
const [filesTotalQuota, setFilesTotalQuota] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const getQuota = async () => {
|
||||
const { FileUploadBytesUsed, FileUploadBytesLimit } = SubscriptionSettingName
|
||||
|
||||
const usedQuota = await application.settings.getSubscriptionSetting(FileUploadBytesUsed)
|
||||
const totalQuota = await application.settings.getSubscriptionSetting(FileUploadBytesLimit)
|
||||
|
||||
setFilesUsedQuota(usedQuota ? parseFloat(usedQuota) : 0)
|
||||
setFilesTotalQuota(totalQuota ? parseFloat(totalQuota) : 0)
|
||||
}
|
||||
|
||||
getQuota().catch(console.error)
|
||||
}, [application.settings])
|
||||
|
||||
const usedQuotaRatioPercent = Math.round((filesUsedQuota * 100) / filesTotalQuota)
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={'Files'} />
|
||||
<StyledSectionedTableCell first>
|
||||
<View>
|
||||
<Title>Storage Quota</Title>
|
||||
<SubTitle>
|
||||
{formatSizeToReadableString(filesUsedQuota)} of {formatSizeToReadableString(filesTotalQuota)} used
|
||||
</SubTitle>
|
||||
<View style={styles.progressBarContainer}>
|
||||
<View style={{ ...styles.progressBar, width: `${usedQuotaRatioPercent}%` }} />
|
||||
</View>
|
||||
</View>
|
||||
</StyledSectionedTableCell>
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
239
packages/mobile/src/Screens/Settings/Sections/OptionsSection.tsx
Normal file
239
packages/mobile/src/Screens/Settings/Sections/OptionsSection.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import { useSignedIn } from '@Lib/SnjsHelperHooks'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { ButtonCell } from '@Root/Components/ButtonCell'
|
||||
import { SectionedAccessoryTableCell } from '@Root/Components/SectionedAccessoryTableCell'
|
||||
import { SectionedOptionsTableCell } from '@Root/Components/SectionedOptionsTableCell'
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
||||
import { SCREEN_MANAGE_SESSIONS, SCREEN_SETTINGS } from '@Root/Screens/screens'
|
||||
import { ButtonType, PrefKey } from '@standardnotes/snjs'
|
||||
import moment from 'moment'
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import DocumentPicker from 'react-native-document-picker'
|
||||
import RNFS from 'react-native-fs'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
encryptionAvailable: boolean
|
||||
}
|
||||
|
||||
export const OptionsSection = ({ title, encryptionAvailable }: Props) => {
|
||||
// Context
|
||||
const application = useContext(ApplicationContext)
|
||||
const [signedIn] = useSignedIn()
|
||||
const navigation = useNavigation<ModalStackNavigationProp<typeof SCREEN_SETTINGS>['navigation']>()
|
||||
|
||||
// State
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [lastExportDate, setLastExportDate] = useState<Date | undefined>(() =>
|
||||
application?.getLocalPreferences().getValue(PrefKey.MobileLastExportDate, undefined),
|
||||
)
|
||||
|
||||
const lastExportData = useMemo(() => {
|
||||
if (lastExportDate) {
|
||||
const formattedDate = moment(lastExportDate).format('lll')
|
||||
const lastExportString = `Last exported on ${formattedDate}`
|
||||
|
||||
// Date is stale if more than 7 days ago
|
||||
const staleThreshold = 7 * 86400
|
||||
const stale =
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore date type issue
|
||||
(new Date() - new Date(lastExportDate)) / 1000 > staleThreshold
|
||||
return {
|
||||
lastExportString,
|
||||
stale,
|
||||
}
|
||||
}
|
||||
return {
|
||||
lastExportString: 'Your data has not yet been backed up.',
|
||||
stale: false,
|
||||
}
|
||||
}, [lastExportDate])
|
||||
|
||||
const email = useMemo(() => {
|
||||
if (signedIn) {
|
||||
const user = application?.getUser()
|
||||
return user?.email
|
||||
}
|
||||
return
|
||||
}, [application, signedIn])
|
||||
|
||||
const exportOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: 'Encrypted',
|
||||
key: 'encrypted',
|
||||
selected: encryptionAvailable,
|
||||
},
|
||||
{ title: 'Decrypted', key: 'decrypted', selected: true },
|
||||
]
|
||||
}, [encryptionAvailable])
|
||||
|
||||
const destroyLocalData = async () => {
|
||||
if (
|
||||
await application?.alertService?.confirm(
|
||||
'Signing out will remove all data from this device, including notes and tags. Make sure your data is synced before proceeding.',
|
||||
'Sign Out?',
|
||||
'Sign Out',
|
||||
ButtonType.Danger,
|
||||
)
|
||||
) {
|
||||
await application!.user.signOut()
|
||||
}
|
||||
}
|
||||
|
||||
const exportData = useCallback(
|
||||
async (encrypted: boolean) => {
|
||||
setExporting(true)
|
||||
const result = await application?.getBackupsService().export(encrypted)
|
||||
if (result) {
|
||||
const exportDate = new Date()
|
||||
setLastExportDate(exportDate)
|
||||
void application?.getLocalPreferences().setUserPrefValue(PrefKey.MobileLastExportDate, exportDate)
|
||||
}
|
||||
setExporting(false)
|
||||
},
|
||||
[application],
|
||||
)
|
||||
|
||||
const readImportFile = async (fileUri: string): Promise<any> => {
|
||||
return RNFS.readFile(fileUri)
|
||||
.then(result => JSON.parse(result))
|
||||
.catch(() => {
|
||||
void application!.alertService!.alert('Unable to open file. Ensure it is a proper JSON file and try again.')
|
||||
})
|
||||
}
|
||||
|
||||
const performImport = async (data: any) => {
|
||||
const result = await application!.mutator.importData(data)
|
||||
if (!result) {
|
||||
return
|
||||
} else if ('error' in result) {
|
||||
void application!.alertService!.alert(result.error.text)
|
||||
} else if (result.errorCount) {
|
||||
void application!.alertService!.alert(
|
||||
`Import complete. ${result.errorCount} items were not imported because ` +
|
||||
'there was an error decrypting them. Make sure the password is correct and try again.',
|
||||
)
|
||||
} else {
|
||||
void application!.alertService!.alert('Your data has been successfully imported.')
|
||||
}
|
||||
}
|
||||
|
||||
const onImportPress = async () => {
|
||||
try {
|
||||
const selectedFiles = await DocumentPicker.pick({
|
||||
type: [DocumentPicker.types.plainText],
|
||||
})
|
||||
const selectedFile = selectedFiles[0]
|
||||
const selectedFileURI = Platform.OS === 'ios' ? decodeURIComponent(selectedFile.uri) : selectedFile.uri
|
||||
const data = await readImportFile(selectedFileURI)
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
setImporting(true)
|
||||
if (data.version || data.auth_params || data.keyParams) {
|
||||
const version = data.version || data.keyParams?.version || data.auth_params?.version
|
||||
if (application!.protocolService.supportedVersions().includes(version)) {
|
||||
await performImport(data)
|
||||
} else {
|
||||
void application!.alertService.alert(
|
||||
'This backup file was created using an unsupported version of the application ' +
|
||||
'and cannot be imported here. Please update your application and try again.',
|
||||
)
|
||||
}
|
||||
} else {
|
||||
await performImport(data)
|
||||
}
|
||||
} finally {
|
||||
setImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onExportPress = useCallback(
|
||||
async (option: { key: string }) => {
|
||||
const encrypted = option.key === 'encrypted'
|
||||
if (encrypted && !encryptionAvailable) {
|
||||
void application?.alertService!.alert(
|
||||
'You must be signed in, or have a local passcode set, to generate an encrypted export file.',
|
||||
'Not Available',
|
||||
'OK',
|
||||
)
|
||||
return
|
||||
}
|
||||
void exportData(encrypted)
|
||||
},
|
||||
[application?.alertService, encryptionAvailable, exportData],
|
||||
)
|
||||
|
||||
const openManageSessions = useCallback(() => {
|
||||
navigation.push(SCREEN_MANAGE_SESSIONS)
|
||||
}, [navigation])
|
||||
|
||||
const showDataBackupAlert = useCallback(() => {
|
||||
void application?.alertService.alert(
|
||||
'Because you are using the app offline without a sync account, it is your responsibility to keep your data safe and backed up. It is recommended you export a backup of your data at least once a week, or, to sign up for a sync account so that your data is backed up automatically.',
|
||||
'No Backups Created',
|
||||
'OK',
|
||||
)
|
||||
}, [application?.alertService])
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={title} />
|
||||
|
||||
{signedIn && (
|
||||
<>
|
||||
<ButtonCell
|
||||
testID="manageSessionsButton"
|
||||
leftAligned={true}
|
||||
first={true}
|
||||
title={'Manage Sessions'}
|
||||
onPress={openManageSessions}
|
||||
/>
|
||||
<ButtonCell
|
||||
testID="signOutButton"
|
||||
leftAligned={true}
|
||||
first={false}
|
||||
title={`Sign out (${email})`}
|
||||
onPress={destroyLocalData}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ButtonCell
|
||||
testID="importData"
|
||||
first={!signedIn}
|
||||
leftAligned
|
||||
title={importing ? 'Processing...' : 'Import Data'}
|
||||
onPress={onImportPress}
|
||||
/>
|
||||
|
||||
<SectionedOptionsTableCell
|
||||
testID="exportData"
|
||||
leftAligned
|
||||
options={exportOptions}
|
||||
title={exporting ? 'Processing...' : 'Export Data'}
|
||||
onPress={onExportPress}
|
||||
/>
|
||||
|
||||
{!signedIn && (
|
||||
<SectionedAccessoryTableCell
|
||||
testID="lastExportDate"
|
||||
onPress={() => {
|
||||
if (!lastExportDate || lastExportData.stale) {
|
||||
showDataBackupAlert()
|
||||
}
|
||||
}}
|
||||
tinted={!lastExportDate || lastExportData.stale}
|
||||
text={lastExportData.lastExportString}
|
||||
/>
|
||||
)}
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { SectionedAccessoryTableCell } from '@Root/Components/SectionedAccessoryTableCell'
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import { CollectionSort, CollectionSortProperty, PrefKey } from '@standardnotes/snjs'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
|
||||
export const PreferencesSection = () => {
|
||||
// Context
|
||||
const application = useSafeApplicationContext()
|
||||
|
||||
// State
|
||||
const [sortBy, setSortBy] = useState<CollectionSortProperty>(() =>
|
||||
application.getPreference(PrefKey.MobileSortNotesBy, CollectionSort.CreatedAt),
|
||||
)
|
||||
const [sortReverse, setSortReverse] = useState<boolean>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileSortNotesReverse, false),
|
||||
)
|
||||
const [hideDates, setHideDates] = useState<boolean>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileNotesHideDate, false),
|
||||
)
|
||||
const [hideEditorIcon, setHideEditorIcon] = useState<boolean>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileNotesHideEditorIcon, false),
|
||||
)
|
||||
const [hidePreviews, setHidePreviews] = useState<boolean>(() =>
|
||||
application.getLocalPreferences().getValue(PrefKey.MobileNotesHideNotePreview, false),
|
||||
)
|
||||
|
||||
const sortOptions = useMemo(() => {
|
||||
return [
|
||||
{ key: CollectionSort.CreatedAt, label: 'Date Added' },
|
||||
{ key: CollectionSort.UpdatedAt, label: 'Date Modified' },
|
||||
{ key: CollectionSort.Title, label: 'Title' },
|
||||
]
|
||||
}, [])
|
||||
|
||||
const toggleReverseSort = () => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileSortNotesReverse, !sortReverse)
|
||||
setSortReverse(value => !value)
|
||||
}
|
||||
|
||||
const changeSortOption = (key: CollectionSortProperty) => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileSortNotesBy, key)
|
||||
setSortBy(key)
|
||||
}
|
||||
const toggleNotesPreviewHidden = () => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileNotesHideNotePreview, !hidePreviews)
|
||||
setHidePreviews(value => !value)
|
||||
}
|
||||
const toggleNotesDateHidden = () => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileNotesHideDate, !hideDates)
|
||||
setHideDates(value => !value)
|
||||
}
|
||||
const toggleNotesEditorIconHidden = () => {
|
||||
void application.getLocalPreferences().setUserPrefValue(PrefKey.MobileNotesHideEditorIcon, !hideEditorIcon)
|
||||
setHideEditorIcon(value => !value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableSection>
|
||||
<SectionHeader
|
||||
title={'Sort Notes By'}
|
||||
buttonText={sortReverse ? 'Disable Reverse Sort' : 'Enable Reverse Sort'}
|
||||
buttonAction={toggleReverseSort}
|
||||
/>
|
||||
{sortOptions.map((option, i) => {
|
||||
return (
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={() => {
|
||||
changeSortOption(option.key)
|
||||
}}
|
||||
text={option.label}
|
||||
key={option.key}
|
||||
first={i === 0}
|
||||
last={i === sortOptions.length - 1}
|
||||
selected={() => option.key === sortBy}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TableSection>
|
||||
|
||||
<TableSection>
|
||||
<SectionHeader title={'Note List Options'} />
|
||||
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={toggleNotesPreviewHidden}
|
||||
text={'Hide note previews'}
|
||||
first
|
||||
selected={() => hidePreviews}
|
||||
/>
|
||||
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={toggleNotesDateHidden}
|
||||
text={'Hide note dates'}
|
||||
selected={() => hideDates}
|
||||
/>
|
||||
|
||||
<SectionedAccessoryTableCell
|
||||
onPress={toggleNotesEditorIconHidden}
|
||||
text={'Hide editor icons'}
|
||||
last
|
||||
selected={() => hideEditorIcon}
|
||||
/>
|
||||
</TableSection>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { SectionedTableCell } from '@Root/Components/SectionedTableCell'
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const BaseView = styled.View``
|
||||
|
||||
export const StyledSectionedTableCell = styled(SectionedTableCell)`
|
||||
padding-top: 12px;
|
||||
`
|
||||
|
||||
export const SubText = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitNeutralColor};
|
||||
font-size: 14px;
|
||||
padding: 12px 14px;
|
||||
`
|
||||
|
||||
export const Subtitle = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitNeutralColor};
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
`
|
||||
|
||||
export const Title = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitForegroundColor};
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
`
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useProtectionSessionExpiry } from '@Lib/SnjsHelperHooks'
|
||||
import { ButtonCell } from '@Root/Components/ButtonCell'
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import React from 'react'
|
||||
import { BaseView, StyledSectionedTableCell, SubText, Subtitle, Title } from './ProtectionsSection.styled'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
protectionsAvailable?: boolean
|
||||
}
|
||||
|
||||
export const ProtectionsSection = (props: Props) => {
|
||||
// Context
|
||||
const application = useSafeApplicationContext()
|
||||
// State
|
||||
const [protectionsDisabledUntil] = useProtectionSessionExpiry()
|
||||
|
||||
const protectionsEnabledSubtitle = protectionsDisabledUntil ? `Disabled until ${protectionsDisabledUntil}` : 'Enabled'
|
||||
|
||||
const protectionsEnabledSubtext =
|
||||
'Actions like viewing protected notes, exporting decrypted backups, or revoking an active session, require additional authentication like entering your account password or application passcode.'
|
||||
|
||||
const enableProtections = () => {
|
||||
void application?.clearProtectionSession()
|
||||
}
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={props.title} />
|
||||
<StyledSectionedTableCell first>
|
||||
<BaseView>
|
||||
<Title>Status</Title>
|
||||
<Subtitle>{props.protectionsAvailable ? protectionsEnabledSubtitle : 'Disabled'}</Subtitle>
|
||||
</BaseView>
|
||||
</StyledSectionedTableCell>
|
||||
{props.protectionsAvailable && protectionsDisabledUntil && (
|
||||
<ButtonCell leftAligned title={'Enable Protections'} onPress={enableProtections} />
|
||||
)}
|
||||
<SubText>{protectionsEnabledSubtext}</SubText>
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const Title = styled.Text`
|
||||
color: ${({ theme }) => theme.stylekitNeutralColor};
|
||||
margin-top: 2px;
|
||||
`
|
||||
@@ -0,0 +1,248 @@
|
||||
import { UnlockTiming } from '@Lib/ApplicationState'
|
||||
import { MobileDeviceInterface } from '@Lib/Interface'
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native'
|
||||
import { ApplicationContext } from '@Root/ApplicationContext'
|
||||
import { ButtonCell } from '@Root/Components/ButtonCell'
|
||||
import { Option, SectionedOptionsTableCell } from '@Root/Components/SectionedOptionsTableCell'
|
||||
import { SectionHeader } from '@Root/Components/SectionHeader'
|
||||
import { TableSection } from '@Root/Components/TableSection'
|
||||
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
||||
import { SCREEN_INPUT_MODAL_PASSCODE, SCREEN_SETTINGS } from '@Root/Screens/screens'
|
||||
import { StorageEncryptionPolicy } from '@standardnotes/snjs'
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react'
|
||||
import { Platform } from 'react-native'
|
||||
import { Title } from './SecuritySection.styled'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
hasPasscode: boolean
|
||||
encryptionAvailable: boolean
|
||||
updateProtectionsAvailable: (...args: unknown[]) => unknown
|
||||
}
|
||||
|
||||
export const SecuritySection = (props: Props) => {
|
||||
const navigation = useNavigation<ModalStackNavigationProp<typeof SCREEN_SETTINGS>['navigation']>()
|
||||
// Context
|
||||
const application = useContext(ApplicationContext)
|
||||
|
||||
// State
|
||||
const [encryptionPolicy, setEncryptionPolicy] = useState(() => application?.getStorageEncryptionPolicy())
|
||||
const [encryptionPolictChangeInProgress, setEncryptionPolictChangeInProgress] = useState(false)
|
||||
const [hasScreenshotPrivacy, setHasScreenshotPrivacy] = useState<boolean | undefined>(false)
|
||||
const [hasBiometrics, setHasBiometrics] = useState(false)
|
||||
const [supportsBiometrics, setSupportsBiometrics] = useState(false)
|
||||
const [biometricsTimingOptions, setBiometricsTimingOptions] = useState(() =>
|
||||
application!.getAppState().getBiometricsTimingOptions(),
|
||||
)
|
||||
const [passcodeTimingOptions, setPasscodeTimingOptions] = useState(() =>
|
||||
application!.getAppState().getPasscodeTimingOptions(),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
const getHasScreenshotPrivacy = async () => {
|
||||
const hasScreenshotPrivacyEnabled = await application?.getAppState().screenshotPrivacyEnabled
|
||||
if (mounted) {
|
||||
setHasScreenshotPrivacy(hasScreenshotPrivacyEnabled)
|
||||
}
|
||||
}
|
||||
void getHasScreenshotPrivacy()
|
||||
const getHasBiometrics = async () => {
|
||||
const appHasBiometrics = await application!.hasBiometrics()
|
||||
if (mounted) {
|
||||
setHasBiometrics(appHasBiometrics)
|
||||
}
|
||||
}
|
||||
void getHasBiometrics()
|
||||
const hasBiometricsSupport = async () => {
|
||||
const hasBiometricsAvailable = await (
|
||||
application?.deviceInterface as MobileDeviceInterface
|
||||
).getDeviceBiometricsAvailability()
|
||||
if (mounted) {
|
||||
setSupportsBiometrics(hasBiometricsAvailable)
|
||||
}
|
||||
}
|
||||
void hasBiometricsSupport()
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [application])
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (props.hasPasscode) {
|
||||
setPasscodeTimingOptions(() => application!.getAppState().getPasscodeTimingOptions())
|
||||
}
|
||||
}, [application, props.hasPasscode]),
|
||||
)
|
||||
|
||||
const toggleEncryptionPolicy = async () => {
|
||||
if (!props.encryptionAvailable) {
|
||||
return
|
||||
}
|
||||
|
||||
if (encryptionPolicy === StorageEncryptionPolicy.Default) {
|
||||
setEncryptionPolictChangeInProgress(true)
|
||||
setEncryptionPolicy(StorageEncryptionPolicy.Disabled)
|
||||
await application?.setStorageEncryptionPolicy(StorageEncryptionPolicy.Disabled)
|
||||
setEncryptionPolictChangeInProgress(false)
|
||||
} else if (encryptionPolicy === StorageEncryptionPolicy.Disabled) {
|
||||
setEncryptionPolictChangeInProgress(true)
|
||||
setEncryptionPolicy(StorageEncryptionPolicy.Default)
|
||||
await application?.setStorageEncryptionPolicy(StorageEncryptionPolicy.Default)
|
||||
setEncryptionPolictChangeInProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
// State
|
||||
const storageEncryptionTitle = props.encryptionAvailable
|
||||
? encryptionPolicy === StorageEncryptionPolicy.Default
|
||||
? 'Disable Storage Encryption'
|
||||
: 'Enable Storage Encryption'
|
||||
: 'Storage Encryption'
|
||||
|
||||
let storageSubText = "Encrypts your data before saving to your device's local storage."
|
||||
|
||||
if (props.encryptionAvailable) {
|
||||
storageSubText +=
|
||||
encryptionPolicy === StorageEncryptionPolicy.Default
|
||||
? ' Disable to improve app start-up speed.'
|
||||
: ' May decrease app start-up speed.'
|
||||
} else {
|
||||
storageSubText += ' Sign in, register, or add a local passcode to enable this option.'
|
||||
}
|
||||
|
||||
if (encryptionPolictChangeInProgress) {
|
||||
storageSubText = 'Applying changes...'
|
||||
}
|
||||
|
||||
const screenshotPrivacyFeatureText =
|
||||
Platform.OS === 'ios' ? 'Multitasking Privacy' : 'Multitasking/Screenshot Privacy'
|
||||
|
||||
const screenshotPrivacyTitle = hasScreenshotPrivacy
|
||||
? `Disable ${screenshotPrivacyFeatureText}`
|
||||
: `Enable ${screenshotPrivacyFeatureText}`
|
||||
|
||||
const passcodeTitle = props.hasPasscode ? 'Disable Passcode Lock' : 'Enable Passcode Lock'
|
||||
|
||||
const biometricTitle = hasBiometrics ? 'Disable Biometrics Lock' : 'Enable Biometrics Lock'
|
||||
|
||||
const setBiometricsTiming = async (timing: UnlockTiming) => {
|
||||
await application?.getAppState().setBiometricsTiming(timing)
|
||||
setBiometricsTimingOptions(() => application!.getAppState().getBiometricsTimingOptions())
|
||||
}
|
||||
|
||||
const setPasscodeTiming = async (timing: UnlockTiming) => {
|
||||
await application?.getAppState().setPasscodeTiming(timing)
|
||||
setPasscodeTimingOptions(() => application!.getAppState().getPasscodeTimingOptions())
|
||||
}
|
||||
|
||||
const onScreenshotPrivacyPress = async () => {
|
||||
const enable = !hasScreenshotPrivacy
|
||||
setHasScreenshotPrivacy(enable)
|
||||
await application?.getAppState().setScreenshotPrivacyEnabled(enable)
|
||||
}
|
||||
|
||||
const onPasscodePress = async () => {
|
||||
if (props.hasPasscode) {
|
||||
void disableAuthentication('passcode')
|
||||
} else {
|
||||
navigation.push(SCREEN_INPUT_MODAL_PASSCODE)
|
||||
}
|
||||
}
|
||||
|
||||
const onBiometricsPress = async () => {
|
||||
if (hasBiometrics) {
|
||||
void disableAuthentication('biometrics')
|
||||
} else {
|
||||
setHasBiometrics(true)
|
||||
await application?.enableBiometrics()
|
||||
await setBiometricsTiming(UnlockTiming.OnQuit)
|
||||
props.updateProtectionsAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
const disableBiometrics = useCallback(async () => {
|
||||
if (await application?.disableBiometrics()) {
|
||||
setHasBiometrics(false)
|
||||
props.updateProtectionsAvailable()
|
||||
}
|
||||
}, [application, props])
|
||||
|
||||
const disablePasscode = useCallback(async () => {
|
||||
const hasAccount = Boolean(application?.hasAccount())
|
||||
let message
|
||||
if (hasAccount) {
|
||||
message =
|
||||
'Are you sure you want to disable your local passcode? This will not affect your encryption status, as your data is currently being encrypted through your sync account keys.'
|
||||
} else {
|
||||
message = 'Are you sure you want to disable your local passcode? This will disable encryption on your data.'
|
||||
}
|
||||
|
||||
const confirmed = await application?.alertService?.confirm(
|
||||
message,
|
||||
'Disable Passcode',
|
||||
'Disable Passcode',
|
||||
undefined,
|
||||
)
|
||||
if (confirmed) {
|
||||
await application?.removePasscode()
|
||||
}
|
||||
}, [application])
|
||||
|
||||
const disableAuthentication = useCallback(
|
||||
async (authenticationMethod: 'passcode' | 'biometrics') => {
|
||||
switch (authenticationMethod) {
|
||||
case 'biometrics': {
|
||||
void disableBiometrics()
|
||||
break
|
||||
}
|
||||
case 'passcode': {
|
||||
void disablePasscode()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
[disableBiometrics, disablePasscode],
|
||||
)
|
||||
|
||||
return (
|
||||
<TableSection>
|
||||
<SectionHeader title={props.title} />
|
||||
|
||||
<ButtonCell first leftAligned title={storageEncryptionTitle} onPress={toggleEncryptionPolicy}>
|
||||
<Title>{storageSubText}</Title>
|
||||
</ButtonCell>
|
||||
|
||||
<ButtonCell leftAligned title={screenshotPrivacyTitle} onPress={onScreenshotPrivacyPress} />
|
||||
|
||||
<ButtonCell leftAligned title={passcodeTitle} onPress={onPasscodePress} />
|
||||
|
||||
<ButtonCell
|
||||
last={!hasBiometrics && !props.hasPasscode}
|
||||
disabled={!supportsBiometrics}
|
||||
leftAligned
|
||||
title={biometricTitle}
|
||||
onPress={onBiometricsPress}
|
||||
/>
|
||||
|
||||
{props.hasPasscode && (
|
||||
<SectionedOptionsTableCell
|
||||
leftAligned
|
||||
title={'Require Passcode'}
|
||||
options={passcodeTimingOptions}
|
||||
onPress={(option: Option) => setPasscodeTiming(option.key as UnlockTiming)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasBiometrics && (
|
||||
<SectionedOptionsTableCell
|
||||
leftAligned
|
||||
title={'Require Biometrics'}
|
||||
options={biometricsTimingOptions}
|
||||
onPress={(option: Option) => setBiometricsTiming(option.key as UnlockTiming)}
|
||||
/>
|
||||
)}
|
||||
</TableSection>
|
||||
)
|
||||
}
|
||||
5
packages/mobile/src/Screens/Settings/Settings.styled.ts
Normal file
5
packages/mobile/src/Screens/Settings/Settings.styled.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components/native'
|
||||
|
||||
export const Container = styled.ScrollView`
|
||||
background-color: ${({ theme }) => theme.stylekitBackgroundColor};
|
||||
`
|
||||
70
packages/mobile/src/Screens/Settings/Settings.tsx
Normal file
70
packages/mobile/src/Screens/Settings/Settings.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useSignedIn } from '@Lib/SnjsHelperHooks'
|
||||
import { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
||||
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
||||
import { SCREEN_SETTINGS } from '@Root/Screens/screens'
|
||||
import { FilesSection } from '@Screens/Settings/Sections/FilesSection'
|
||||
import { FeatureIdentifier } from '@standardnotes/features'
|
||||
import { ApplicationEvent, FeatureStatus } from '@standardnotes/snjs'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { AuthSection } from './Sections/AuthSection'
|
||||
import { CompanySection } from './Sections/CompanySection'
|
||||
import { EncryptionSection } from './Sections/EncryptionSection'
|
||||
import { OptionsSection } from './Sections/OptionsSection'
|
||||
import { PreferencesSection } from './Sections/PreferencesSection'
|
||||
import { ProtectionsSection } from './Sections/ProtectionsSection'
|
||||
import { SecuritySection } from './Sections/SecuritySection'
|
||||
import { Container } from './Settings.styled'
|
||||
|
||||
type Props = ModalStackNavigationProp<typeof SCREEN_SETTINGS>
|
||||
export const Settings = (props: Props) => {
|
||||
// Context
|
||||
const application = useSafeApplicationContext()
|
||||
|
||||
// State
|
||||
const [hasPasscode, setHasPasscode] = useState(() => Boolean(application.hasPasscode()))
|
||||
const [protectionsAvailable, setProtectionsAvailable] = useState(application.hasProtectionSources())
|
||||
const [encryptionAvailable, setEncryptionAvailable] = useState(() => application.isEncryptionAvailable())
|
||||
|
||||
const updateProtectionsAvailable = useCallback(() => {
|
||||
setProtectionsAvailable(application.hasProtectionSources())
|
||||
}, [application])
|
||||
|
||||
useEffect(() => {
|
||||
const removeApplicationEventSubscriber = application.addEventObserver(async event => {
|
||||
if (event === ApplicationEvent.KeyStatusChanged) {
|
||||
setHasPasscode(Boolean(application.hasPasscode()))
|
||||
updateProtectionsAvailable()
|
||||
setEncryptionAvailable(() => application.isEncryptionAvailable())
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
removeApplicationEventSubscriber && removeApplicationEventSubscriber()
|
||||
}
|
||||
}, [application, updateProtectionsAvailable])
|
||||
|
||||
const goBack = useCallback(() => {
|
||||
props.navigation.goBack()
|
||||
}, [props.navigation])
|
||||
|
||||
const [signedIn] = useSignedIn(goBack)
|
||||
|
||||
const isEntitledToFiles = application.features.getFeatureStatus(FeatureIdentifier.Files) === FeatureStatus.Entitled
|
||||
|
||||
return (
|
||||
<Container keyboardShouldPersistTaps={'always'} keyboardDismissMode={'interactive'}>
|
||||
<AuthSection title="Account" signedIn={signedIn} />
|
||||
<OptionsSection encryptionAvailable={!!encryptionAvailable} title="Options" />
|
||||
<PreferencesSection />
|
||||
{application.hasAccount() && isEntitledToFiles && <FilesSection />}
|
||||
<SecuritySection
|
||||
encryptionAvailable={!!encryptionAvailable}
|
||||
hasPasscode={hasPasscode}
|
||||
updateProtectionsAvailable={updateProtectionsAvailable}
|
||||
title="Security"
|
||||
/>
|
||||
<ProtectionsSection title="Protections" protectionsAvailable={protectionsAvailable} />
|
||||
<EncryptionSection encryptionAvailable={!!encryptionAvailable} title={'Encryption Status'} />
|
||||
<CompanySection title="Standard Notes" />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user