267 lines
8.9 KiB
TypeScript
267 lines
8.9 KiB
TypeScript
import { AlwaysOpenWebAppOnLaunchKey } from '@Lib/constants'
|
|
import { useSignedIn } from '@Lib/SnjsHelperHooks'
|
|
import { useNavigation } from '@react-navigation/native'
|
|
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 { useSafeApplicationContext } from '@Root/Hooks/useSafeApplicationContext'
|
|
import { ModalStackNavigationProp } from '@Root/ModalStack'
|
|
import { SCREEN_MANAGE_SESSIONS, SCREEN_SETTINGS, SCREEN_WEB_APP } from '@Root/Screens/screens'
|
|
import { ButtonType, PrefKey, StorageValueModes } from '@standardnotes/snjs'
|
|
import moment from 'moment'
|
|
import React, { useCallback, useEffect, 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 = useSafeApplicationContext()
|
|
|
|
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 () => {
|
|
let signoutText =
|
|
'Signing out will remove all data from this device, including notes and tags. Make sure your data is synced before proceeding.'
|
|
|
|
signoutText += '\n\nYour app will quit after sign out completes.'
|
|
|
|
if (await application.alertService.confirm(signoutText, '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])
|
|
|
|
const [shouldAlwaysOpenWebAppOnLaunch, setShouldAlwaysOpenWebAppOnLaunch] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const getSetting = async () => {
|
|
const value = (await application.getValue(AlwaysOpenWebAppOnLaunchKey, StorageValueModes.Nonwrapped)) as
|
|
| boolean
|
|
| undefined
|
|
setShouldAlwaysOpenWebAppOnLaunch(value ?? false)
|
|
}
|
|
void getSetting()
|
|
}, [application])
|
|
|
|
const openWebApp = useCallback(() => {
|
|
navigation.push(SCREEN_WEB_APP)
|
|
}, [navigation])
|
|
|
|
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}
|
|
/>
|
|
|
|
<ButtonCell testID="openWebApp" leftAligned title="Open Web App" onPress={() => openWebApp()} />
|
|
<SectionedAccessoryTableCell
|
|
onPress={() => {
|
|
const newValue = !shouldAlwaysOpenWebAppOnLaunch
|
|
setShouldAlwaysOpenWebAppOnLaunch(newValue)
|
|
void application.setValue(AlwaysOpenWebAppOnLaunchKey, newValue, StorageValueModes.Nonwrapped)
|
|
}}
|
|
text="Always Open Web App On Launch"
|
|
selected={() => shouldAlwaysOpenWebAppOnLaunch}
|
|
/>
|
|
|
|
{!signedIn && (
|
|
<SectionedAccessoryTableCell
|
|
testID="lastExportDate"
|
|
onPress={() => {
|
|
if (!lastExportDate || lastExportData.stale) {
|
|
showDataBackupAlert()
|
|
}
|
|
}}
|
|
tinted={!lastExportDate || lastExportData.stale}
|
|
text={lastExportData.lastExportString}
|
|
/>
|
|
)}
|
|
</TableSection>
|
|
)
|
|
}
|