-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #25 from displaynone:save-restore
feat: added backup and restore sites
- Loading branch information
Showing
19 changed files
with
1,890 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -73,4 +73,6 @@ android/ | |
|
||
.env | ||
|
||
# @end expo-cli | ||
# @end expo-cli | ||
|
||
.eslintcache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { Trans } from '@lingui/macro'; | ||
import * as FileSystem from 'expo-file-system'; | ||
import * as Sharing from 'expo-sharing'; | ||
import React, { FC } from 'react'; | ||
import { Dimensions, StyleSheet, View } from 'react-native'; | ||
import { Button, MD3Theme, useTheme } from 'react-native-paper'; | ||
import { ExportIcon } from '../../src/icons/Export'; | ||
import Site from '../../src/models/Site'; | ||
import { useDB } from '../../src/providers/DatabaseProvider'; | ||
import Container from '../../src/ui/Container'; | ||
import Text from '../../src/ui/Text'; | ||
import { format } from 'date-fns'; | ||
|
||
const SettingsExport: FC = () => { | ||
const { listSites } = useDB(); | ||
const theme = useTheme(); | ||
const styles = getStyles(theme); | ||
|
||
const downloadStringAsFile = async () => { | ||
const sites = await listSites(); | ||
const content = JSON.stringify( | ||
sites.map( | ||
site => | ||
({ | ||
label: site.label, | ||
secret: site.secret, | ||
algorithm: site.algorithm, | ||
digits: site.digits, | ||
issuer: site.issuer, | ||
type: site.type, | ||
period: site.period, | ||
} as Site), | ||
), | ||
null, | ||
2, | ||
); | ||
|
||
const fileUri = | ||
(FileSystem.documentDirectory || '') + | ||
'shield-authenticator_' + | ||
format(Date.now(), 'yyyy-MM-dd-HH-mm-ss') + | ||
'.json'; | ||
await FileSystem.writeAsStringAsync(fileUri, content); | ||
|
||
const isAvailable = await Sharing.isAvailableAsync(); | ||
if (!isAvailable) { | ||
alert('Sharing is not available on your device.'); | ||
return; | ||
} | ||
|
||
await Sharing.shareAsync(fileUri); | ||
}; | ||
|
||
return ( | ||
<Container> | ||
<View style={styles.container}> | ||
<Text size="headlineSmall" variant={['bold', 'primary']}> | ||
<Trans>Backup your data</Trans> | ||
</Text> | ||
<Text size="bodyLarge" variant={'secondary'} numberOfLines={3}> | ||
<Trans> | ||
To ensure that you don't lose your saved data, you can download a | ||
file of the sites and restore them at a later time | ||
</Trans> | ||
</Text> | ||
|
||
<ExportIcon | ||
width={Dimensions.get('screen').width - 48} | ||
height={Dimensions.get('screen').width - 48} | ||
/> | ||
<View style={styles.buttonContainer}> | ||
<Button mode="contained" onPress={() => downloadStringAsFile()}> | ||
<Trans>Generate file</Trans> | ||
</Button> | ||
</View> | ||
</View> | ||
</Container> | ||
); | ||
}; | ||
|
||
const getStyles = (theme: MD3Theme) => | ||
StyleSheet.create({ | ||
container: { | ||
flex: 1, | ||
padding: 0, | ||
margin: 0, | ||
}, | ||
buttonContainer: { | ||
marginTop: 24, | ||
}, | ||
}); | ||
|
||
export default SettingsExport; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { Plural, Trans } from '@lingui/macro'; | ||
import * as FileSystem from 'expo-file-system'; | ||
import * as Sharing from 'expo-sharing'; | ||
import React, { FC, useState } from 'react'; | ||
import { Dimensions, StyleSheet, View } from 'react-native'; | ||
import { Button, MD3Theme, ProgressBar, useTheme } from 'react-native-paper'; | ||
import { ImportIcon } from '../../src/icons/Import'; | ||
import Site from '../../src/models/Site'; | ||
import { useDB } from '../../src/providers/DatabaseProvider'; | ||
import Container from '../../src/ui/Container'; | ||
import Text from '../../src/ui/Text'; | ||
import * as DocumentPicker from 'expo-document-picker'; | ||
import { OtpRecord } from '../../src/types'; | ||
|
||
const SettingsImport: FC = () => { | ||
const { newSite } = useDB(); | ||
const theme = useTheme(); | ||
const styles = getStyles(theme); | ||
const [processing, setProcessing] = useState(false); | ||
const [numberOfSites, setNumberOfSites] = useState(0); | ||
const [sitesProcessed, setSitesProcessed] = useState(0); | ||
|
||
const loadBackupFile = async () => { | ||
const result = await DocumentPicker.getDocumentAsync({}); | ||
if (result.type === 'success') { | ||
const content = await FileSystem.readAsStringAsync(result.uri); | ||
try { | ||
const sites = JSON.parse(content) as Site[]; | ||
if (!sites.length) { | ||
throw new Error(); | ||
} | ||
setNumberOfSites(sites.length); | ||
setProcessing(true); | ||
for (let i = 0; i < sites.length; i++) { | ||
const site = sites[i]; | ||
await newSite( | ||
{ | ||
algorithm: site.algorithm, | ||
digits: site.digits, | ||
label: site.label, | ||
period: site.period, | ||
secret: site.secret, | ||
type: site.type, | ||
issuer: site.issuer, | ||
} as OtpRecord, | ||
false, | ||
); | ||
setSitesProcessed(value => value + 1); | ||
} | ||
} catch (e) { | ||
alert('The format of the file is not valid'); | ||
} | ||
} | ||
}; | ||
|
||
return ( | ||
<Container> | ||
<View style={styles.container}> | ||
<Text size="headlineSmall" variant={['bold', 'primary']}> | ||
<Trans>Restore sites</Trans> | ||
</Text> | ||
<Text size="bodyLarge" variant={'secondary'} numberOfLines={5}> | ||
<Trans> | ||
If you want to restore your saved sites from a backup, you can load | ||
the file stored on your device. It's important to note that the | ||
existing sites will remain unchanged and won't be replaced | ||
</Trans> | ||
</Text> | ||
|
||
<ImportIcon | ||
width={Dimensions.get('screen').width - 48} | ||
height={Dimensions.get('screen').width - 48} | ||
/> | ||
{processing && ( | ||
<View style={styles.buttonContainer}> | ||
<Text size="labelLarge" variant={['bold', 'primary', 'marginless']}> | ||
{sitesProcessed !== numberOfSites && <Trans>Processing</Trans>} | ||
{sitesProcessed === numberOfSites && <Trans>Completed</Trans>} | ||
</Text> | ||
<View style={styles.processContainer}> | ||
<ProgressBar progress={0} /> | ||
</View> | ||
<Text> | ||
<Plural | ||
value={sitesProcessed} | ||
one={<Trans>Processed 1 site of {numberOfSites}</Trans>} | ||
other={<Trans>Processed # sites of {numberOfSites}</Trans>} | ||
/> | ||
</Text> | ||
</View> | ||
)} | ||
{!processing && ( | ||
<View style={styles.buttonContainer}> | ||
<Button | ||
mode="contained" | ||
onPress={() => loadBackupFile()} | ||
disabled={processing} | ||
> | ||
<Trans>Load backup sites</Trans> | ||
</Button> | ||
</View> | ||
)} | ||
</View> | ||
</Container> | ||
); | ||
}; | ||
|
||
const getStyles = (theme: MD3Theme) => | ||
StyleSheet.create({ | ||
container: { | ||
flex: 1, | ||
padding: 0, | ||
margin: 0, | ||
}, | ||
buttonContainer: { | ||
marginTop: 24, | ||
}, | ||
processContainer: { | ||
marginVertical: 16, | ||
}, | ||
}); | ||
|
||
export default SettingsImport; |
Binary file not shown.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.