add user deletion functionality

This commit is contained in:
leander committed 2024-08-02 03:05:57 +02:00
1 parent 47d6f63a0e
commit 0531506eb9
11 files changed
+241 -4

No files matched your search

+8
View File
@@ -9,6 +9,7 @@ import EventDetail from 'src/modules/events/EventDetail';
import EventList from 'src/modules/events/EventList';
import Login from 'src/modules/login/Login';
import Membership from 'src/modules/membership/Membership';
import UserDelete from 'src/modules/membership/UserDelete';
import UserRegister from 'src/modules/register/UserRegister';
import theme from 'src/theme';
import type { EventStackParamList, HomeTabParamList, UserStackParamList } from 'src/types/navigation';
@@ -54,6 +55,13 @@ const UserStackScreen = () => (
title: 'Bli medlem',
}}
/>
<UserStack.Screen
name="UserDelete"
component={UserDelete}
options={{
title: 'Slett bruker',
}}
/>
</UserStack.Navigator>
);
+29
View File
@@ -10,6 +10,9 @@ const USER_DATA_FAILURE = 'USER_DATA_FAILURE';
const REGISTER_USER_REQUEST = 'REGISTER_USER_REQUEST';
const REGISTER_USER_SUCCESS = 'REGISTER_USER_SUCCESS';
const REGISTER_USER_FAILURE = 'REGISTER_USER_FAILURE';
const DELETE_USER_REQUEST = 'DELETE_USER_REQUEST';
const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS';
const DELETE_USER_FAILURE = 'DELETE_USER_FAILURE';
function loginRequest() {
return {
@@ -78,6 +81,25 @@ function registerUserFailure(registerError: Record<string, string>) {
};
}
function deleteUserRequest() {
return {
type: DELETE_USER_REQUEST,
};
}
function deleteUserSuccess() {
return {
type: DELETE_USER_SUCCESS,
};
}
function deleteUserFailure(deleteUserError: Record<string, string>) {
return {
type: DELETE_USER_FAILURE,
deleteUserError,
};
}
export {
/* Login */
LOGIN_REQUEST,
@@ -103,4 +125,11 @@ export {
registerUserSuccess,
REGISTER_USER_FAILURE,
registerUserFailure,
/* Delete */
DELETE_USER_REQUEST,
deleteUserRequest,
DELETE_USER_SUCCESS,
deleteUserSuccess,
DELETE_USER_FAILURE,
deleteUserFailure,
};
+26
View File
@@ -8,6 +8,9 @@ import {
userDataFailure,
userDataRequest,
userDataSuccess,
deleteUserFailure,
deleteUserSuccess,
deleteUserRequest,
} from './actions';
import type { UserRegisterFormValues } from './modules/register/useUserRegister';
import { AppDispatch } from './reduxStore';
@@ -116,6 +119,29 @@ export function requestRegisterUser({ first_name, last_name, email, phone_number
};
}
export function requestDeleteUser(authToken: string) {
return (dispatch: AppDispatch) => {
dispatch(deleteUserRequest());
return fetchWithTimeout(`${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/me/delete/`, {
method: 'DELETE',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${authToken}`,
},
}).then((response) => {
if (!response.ok) {
return response.json().then((json) => {
dispatch(deleteUserFailure(json || { non_field_errors: ['Kunne ikke slette bruker, prøv igjen'] }));
});
}
dispatch(deleteUserSuccess());
return {};
});
};
}
/** Create a stripe payment sheet. Note that this is not using redux */
export async function requestStripePaymentSheet(authToken: string, membershipType: string) {
const body = JSON.stringify({
+15 -3
View File
@@ -9,8 +9,15 @@ import Proof from './Proof';
import useMembership from './useMembership';
const Membership = () => {
const { isFetchingUserData, onRegisterPress, onLoginPress, onLogoutPress, isAuthenticated, fetchUser } =
useMembership();
const {
isFetchingUserData,
onRegisterPress,
onLoginPress,
onLogoutPress,
onDeletePress,
isAuthenticated,
fetchUser,
} = useMembership();
if (isFetchingUserData) {
return (
@@ -23,7 +30,12 @@ const Membership = () => {
if (isAuthenticated) {
return (
<>
<Proof onLogoutPress={onLogoutPress} fetchUser={fetchUser} isFetchingUserData={isFetchingUserData} />
<Proof
onLogoutPress={onLogoutPress}
fetchUser={fetchUser}
isFetchingUserData={isFetchingUserData}
onDeletePress={onDeletePress}
/>
<PrivacyButton />
</>
);
+13 -1
View File
@@ -144,8 +144,9 @@ interface ProofProps {
onLogoutPress: () => void;
fetchUser: () => void;
isFetchingUserData: boolean;
onDeletePress: () => void;
}
const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData }: ProofProps) => {
const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData, onDeletePress }: ProofProps) => {
const CONFETTI_TIMEOUT = 15000;
const confettiRef = useRef<Confetti>(null);
const { paymentLoading, isLoadingMembershipType, onPurchasePress, membershipPrice, toast, setToast, user } = useProof(
@@ -203,6 +204,9 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData }: ProofProps) =>
<Text>Logg ut</Text>
</Button>
</View>
<Button onPress={onDeletePress} style={styles.deleteButton} small>
<Text style={styles.whiteText}>Slett bruker</Text>
</Button>
</ScrollView>
</View>
<Toast
@@ -275,6 +279,14 @@ const styles = StyleSheet.create({
alignSelf: 'center',
marginVertical: 20,
},
deleteButton: {
alignSelf: 'center',
marginVertical: 20,
backgroundColor: theme.colors.danger,
},
whiteText: {
color: theme.colors.neufWhite,
},
purchaseText: {
alignItems: 'center',
},
+97
View File
@@ -0,0 +1,97 @@
import React from 'react';
import { ActivityIndicator, ScrollView, Text, TextInput, View, StyleSheet } from 'react-native';
import { Button } from 'react-native-ui-lib';
import FormErrors from 'src/components/FormErrors';
import { useAppSelector } from 'src/hooks';
import theme from 'src/theme';
import useUserDelete from './useUserDelete';
const UserDelete = () => {
const [value, onChangeText] = React.useState('');
const user = useAppSelector((state) => state.user);
const { onPress, isDeletingUser, deleteUserError } = useUserDelete();
if (!user) {
return null;
}
const isDisabled = !isDeletingUser && value !== user.username;
return (
<ScrollView>
<View style={theme.card}>
<Text style={styles.headerTitle}>Slett meg</Text>
<Text style={styles.paragraph}>
Dette sletter kontoen med brukernavn <Text style={styles.bold}>{user.username}</Text> inkludert all tilknyttet
persondata <Text style={styles.bold}>for alltid</Text>.
</Text>
<Text style={styles.paragraph}>Skriv inn brukernavnet ditt for å fortsette.</Text>
<TextInput
editable
numberOfLines={1}
maxLength={40}
onChangeText={(text) => onChangeText(text)}
value={value}
style={styles.input}
/>
{deleteUserError && <FormErrors errors={deleteUserError} />}
<Button
onPress={onPress}
disabled={isDisabled}
style={!isDisabled ? styles.deleteButton : styles.deleteButtonDisabled}
>
<Text style={!isDisabled ? styles.whiteText : styles.disabledText}>Jeg forstår, slett kontoen min</Text>
</Button>
{isDeletingUser && <ActivityIndicator color={theme.colors.neufOrange} />}
</View>
</ScrollView>
);
};
export default UserDelete;
const styles = StyleSheet.create({
paragraph: {
textAlign: 'center',
fontSize: 16,
color: theme.colors.neufBlack,
paddingBottom: 10,
lineHeight: 25,
},
bold: {
fontWeight: 'bold',
},
deleteButton: {
marginTop: 16,
marginBottom: 16,
backgroundColor: theme.colors.danger,
},
deleteButtonDisabled: {
marginTop: 16,
marginBottom: 16,
backgroundColor: theme.colors.neufGray,
},
whiteText: {
color: theme.colors.neufWhite,
},
disabledText: {
opacity: 0.5,
},
headerTitle: {
textAlign: 'center',
fontWeight: 'bold',
fontSize: 20,
color: theme.colors.neufBlack,
paddingVertical: 10,
lineHeight: 25,
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
margin: 10,
padding: 10,
},
});
+2
View File
@@ -19,6 +19,7 @@ const useMembership = () => {
};
const onRegisterPress = () => navigation.navigate('UserStack', { screen: 'UserRegister' });
const onLoginPress = () => navigation.navigate('UserStack', { screen: 'Login' });
const onDeletePress = () => navigation.navigate('UserStack', { screen: 'UserDelete' });
const fetchUser = useCallback(() => {
dispatch(requestUserData(userToken || ''));
}, [dispatch, userToken]);
@@ -35,6 +36,7 @@ const useMembership = () => {
userToken,
onLogoutPress,
onRegisterPress,
onDeletePress,
onLoginPress,
fetchUser,
};
+32
View File
@@ -0,0 +1,32 @@
import { useNavigation } from '@react-navigation/native';
import { useEffect } from 'react';
import { requestDeleteUser } from 'src/api';
import { useAppDispatch, useAppSelector, usePrevious } from 'src/hooks';
const useUserDelete = () => {
const dispatch = useAppDispatch();
const navigation = useNavigation();
const isAuthenticated = useAppSelector((store) => store.isAuthenticated);
const isAuthenticatedPrev = usePrevious(isAuthenticated);
const userToken = useAppSelector((store) => store.userToken);
const isDeletingUser = useAppSelector((store) => store.isDeletingUser);
const deleteUserError = useAppSelector((store) => store.deleteUserError);
const onPress = () => {
dispatch(requestDeleteUser(userToken || ''));
};
useEffect(() => {
if (isAuthenticatedPrev !== isAuthenticated && !isAuthenticated) {
navigation.navigate('UserStack', { screen: 'Membership' });
}
}, [isAuthenticatedPrev, isAuthenticated, navigation]);
return {
onPress,
isDeletingUser,
deleteUserError,
};
};
export default useUserDelete;
+17
View File
@@ -9,6 +9,9 @@ import {
REGISTER_USER_REQUEST,
REGISTER_USER_FAILURE,
REGISTER_USER_SUCCESS,
DELETE_USER_REQUEST,
DELETE_USER_FAILURE,
DELETE_USER_SUCCESS,
} from './actions';
import type { UserType } from './types/data';
@@ -17,11 +20,13 @@ interface RootState {
isLoggingIn: boolean;
isFetchingUserData: boolean;
isRegisteringUser: boolean;
isDeletingUser: boolean;
userToken: string | null;
user: UserType | null;
loginError: Record<string, string> | null;
userError: Record<string, string> | null;
registerError: Record<string, string> | null;
deleteUserError: Record<string, string> | null;
}
const initialState: RootState = {
@@ -29,11 +34,13 @@ const initialState: RootState = {
isLoggingIn: false,
isFetchingUserData: false,
isRegisteringUser: false,
isDeletingUser: false,
userToken: null,
user: null,
loginError: null,
userError: null,
registerError: null,
deleteUserError: null,
};
// FIXME: refactor using redux slices
@@ -79,6 +86,16 @@ export default function appReducers(state = initialState, action: any) {
return { ...state, isRegisteringUser: false, isAuthenticated: true, user: userData, userToken: token };
/* Delete user */
case DELETE_USER_REQUEST:
return { ...state, isDeletingUser: true, deleteUserError: null };
case DELETE_USER_FAILURE:
return { ...state, isDeletingUser: false, deleteUserError: action.deleteUserError };
case DELETE_USER_SUCCESS:
return initialState;
default:
return state;
}
+1
View File
@@ -30,4 +30,5 @@ export interface UserType {
last_membership: MembershipType | null;
is_volunteer: boolean;
is_member: boolean;
username: string | null;
}
+1
View File
@@ -14,6 +14,7 @@ export type UserStackParamList = {
Membership: undefined;
Login: undefined;
UserRegister: undefined;
UserDelete: undefined;
};
export type HomeTabParamList = {