add user deletion functionality
This commit is contained in:
11 files changed
+241
-4
No files matched your search
@@ -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>
|
||||
);
|
||||
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -30,4 +30,5 @@ export interface UserType {
|
||||
last_membership: MembershipType | null;
|
||||
is_volunteer: boolean;
|
||||
is_member: boolean;
|
||||
username: string | null;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export type UserStackParamList = {
|
||||
Membership: undefined;
|
||||
Login: undefined;
|
||||
UserRegister: undefined;
|
||||
UserDelete: undefined;
|
||||
};
|
||||
|
||||
export type HomeTabParamList = {
|
||||
|
||||
Reference in new issue
Block a user