From 164d008b6e0a0e91aea4f0fab71837356b51b5d2 Mon Sep 17 00:00:00 2001 From: Luis Confraria Date: Wed, 21 Sep 2022 19:18:38 +0100 Subject: [PATCH 1/2] add delete account --- src/context/AuthContext.tsx | 40 ++++++++++++++++++++++++++----- src/hooks/use-delete-account.ts | 38 +++++++++++++++++++++++++++++ src/screens/SettingsScreen.tsx | 42 +++++++++++++++++++++++++++++++-- 3 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 src/hooks/use-delete-account.ts diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 8c7805d5..7f1af7e2 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -61,6 +61,7 @@ type ContextType = { handleLoginGoogle: () => void handleLoginApple: () => void handleLogout: () => Promise + handleDeleteAccount: () => Promise } const AuthContext = React.createContext(undefined) @@ -125,6 +126,7 @@ function useAppleAuth() { export const AuthProvider: React.FC = ({ children, }) => { + const [firebaseUser, setFirebaseUser] = useState() const [loading, setLoading] = useState(true) const [user, setUser] = useState() @@ -132,24 +134,42 @@ export const AuthProvider: React.FC = ({ const handleLoginApple = useAppleAuth() useEffect(() => { - firebase.auth().onAuthStateChanged(async firebaseUser => { + firebase.auth().onAuthStateChanged(newFirebaseUser => { setLoading(false) - if (!firebaseUser) return + setFirebaseUser(newFirebaseUser) + }) + }, []) + useEffect(() => { + const updateUser = async () => { + if (!firebaseUser) { + setUser(null) + return + } + const idToken = await firebaseUser.getIdToken() setUser({ name: firebaseUser.displayName, email: firebaseUser.email, uid: firebaseUser.uid, - idToken: await firebaseUser.getIdToken(), + idToken, }) - }) - }, []) + } + + updateUser() + }, [firebaseUser]) const handleLogout = useCallback(async () => { await firebase.auth().signOut() setUser(null) }, []) + const handleDeleteAccount = useCallback< + ContextType['handleLogout'] + >(async () => { + if (firebaseUser) await firebaseUser.delete() + setUser(null) + }, [firebaseUser]) + const context = useMemo( () => ({ loading, @@ -157,8 +177,16 @@ export const AuthProvider: React.FC = ({ handleLogout, handleLoginApple, handleLoginGoogle, + handleDeleteAccount, }), - [user, loading, handleLogout, handleLoginGoogle, handleLoginApple] + [ + user, + loading, + handleLogout, + handleLoginGoogle, + handleLoginApple, + handleDeleteAccount, + ] ) return {children} diff --git a/src/hooks/use-delete-account.ts b/src/hooks/use-delete-account.ts new file mode 100644 index 00000000..9414619e --- /dev/null +++ b/src/hooks/use-delete-account.ts @@ -0,0 +1,38 @@ +import { useCallback, useMemo } from 'react' + +import { useAuth } from '../context/AuthContext' +import { useSecrets } from '../context/SecretsContext' +import apiFactory from '../lib/api' + +export const useDeleteAccount = () => { + const { user, handleDeleteAccount } = useAuth() + const api = useMemo(() => apiFactory({ idToken: user.idToken }), [user]) + const { secrets, replace } = useSecrets() + + const deleteAllTokens = useCallback(async () => { + const deletePromises = [] + + secrets.forEach(({ tokens }) => { + if (Array.isArray(tokens)) { + tokens.forEach(t => { + deletePromises.push(api.revokeToken(t.token)) + }) + } + }) + await Promise.all(deletePromises) + }, [api, secrets]) + + const deleteAllSecrets = useCallback(async () => { + await replace([]) + }, [replace]) + + const deleteAccount = useCallback(async () => { + await deleteAllTokens() + await deleteAllSecrets() + await handleDeleteAccount() + }, [deleteAllTokens, deleteAllSecrets, handleDeleteAccount]) + + return { + deleteAccount, + } +} diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 50a53f29..05a94a3c 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,5 +1,5 @@ -import React from 'react' -import { ScrollView, StyleSheet, View } from 'react-native' +import React, { useCallback, useState } from 'react' +import { Alert, ScrollView, StyleSheet, View } from 'react-native' import { Switch, Button } from 'react-native-paper' import { StackNavigationProp } from '@react-navigation/stack' import * as DocumentPicker from 'expo-document-picker' @@ -17,6 +17,23 @@ import { readFile, showImportConfirmAlert, } from '../lib/importExport' +import { useDeleteAccount } from '../hooks/use-delete-account' +import { LoadingSpinnerOverlay } from '../components/LoadingSpinnerOverlay' + +const showDeleteAccountConfirm = (onConfirm: () => void) => { + Alert.alert( + '⚠️ DELETE USER ACCOUNT', + 'This actions is irreversible. It will revoke all your tokens and secrets and delete your account from the server. Are you sure you want to continue?', + [ + { + text: 'CANCEL', + style: 'cancel', + }, + { text: 'Delete account', onPress: onConfirm }, + ], + { cancelable: true } + ) +} const styles = StyleSheet.create({ container: { @@ -40,6 +57,9 @@ const styles = StyleSheet.create({ paddingTop: theme.spacing(3), alignItems: 'center', }, + buttonDelete: { + marginTop: theme.spacing(10), + }, }) type SettingsScreenProps = { @@ -49,10 +69,12 @@ type SettingsScreenProps = { export const SettingsScreen: React.FC = ({ navigation, }) => { + const [deletingAccount, setDeletingAccount] = useState(false) const { prefs, save } = usePrefs() const canUseLocalAuth = useCanUseLocalAuth() const { user, handleLogout } = useAuth() const { secrets, replace } = useSecrets() + const { deleteAccount } = useDeleteAccount() const handleImport = async () => { try { @@ -87,6 +109,13 @@ export const SettingsScreen: React.FC = ({ navigation.navigate('ExportFileSecret') } + const handleDeleteAccount = useCallback(async () => { + Toast.show('Deleting account and data') + setDeletingAccount(true) + await deleteAccount() + Toast.show('Account deleted') + }, [deleteAccount, setDeletingAccount]) + return ( @@ -134,9 +163,18 @@ export const SettingsScreen: React.FC = ({ + + {deletingAccount && } ) } From 99d0bc63b4368785dcef266d9a18ff97fc0f1af1 Mon Sep 17 00:00:00 2001 From: Luis Confraria Date: Wed, 21 Sep 2022 19:22:47 +0100 Subject: [PATCH 2/2] fix: test snapshots --- .../SettingsScreen.test.tsx.snap.android | 104 ++++++++++++++++++ src/screens/SettingsScreen.test.tsx.snap.ios | 104 ++++++++++++++++++ 2 files changed, 208 insertions(+) diff --git a/src/screens/SettingsScreen.test.tsx.snap.android b/src/screens/SettingsScreen.test.tsx.snap.android index 9ef8ae63..67b49160 100644 --- a/src/screens/SettingsScreen.test.tsx.snap.android +++ b/src/screens/SettingsScreen.test.tsx.snap.android @@ -553,6 +553,110 @@ exports[`SettingsScreen should match snapshot 1`] = ` + + + + + Delete account + + + + diff --git a/src/screens/SettingsScreen.test.tsx.snap.ios b/src/screens/SettingsScreen.test.tsx.snap.ios index 6fbed57c..d3056781 100644 --- a/src/screens/SettingsScreen.test.tsx.snap.ios +++ b/src/screens/SettingsScreen.test.tsx.snap.ios @@ -552,6 +552,110 @@ exports[`SettingsScreen should match snapshot 1`] = ` + + + + + Delete account + + + +