diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 2bc5048..2f75d50 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -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', }} /> + ); diff --git a/src/actions.ts b/src/actions.ts index 063cf11..d03846a 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -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) { }; } +function deleteUserRequest() { + return { + type: DELETE_USER_REQUEST, + }; +} + +function deleteUserSuccess() { + return { + type: DELETE_USER_SUCCESS, + }; +} + +function deleteUserFailure(deleteUserError: Record) { + 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, }; diff --git a/src/api.ts b/src/api.ts index 331d897..43ae88a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -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({ diff --git a/src/modules/membership/Membership.tsx b/src/modules/membership/Membership.tsx index 7cbcfc0..b534a99 100644 --- a/src/modules/membership/Membership.tsx +++ b/src/modules/membership/Membership.tsx @@ -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 ( <> - + ); diff --git a/src/modules/membership/Proof.tsx b/src/modules/membership/Proof.tsx index 1d37617..bb8c8be 100644 --- a/src/modules/membership/Proof.tsx +++ b/src/modules/membership/Proof.tsx @@ -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(null); const { paymentLoading, isLoadingMembershipType, onPurchasePress, membershipPrice, toast, setToast, user } = useProof( @@ -203,6 +204,9 @@ const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData }: ProofProps) => Logg ut + { + 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 ( + + + Slett meg + + Dette sletter kontoen med brukernavn {user.username} inkludert all tilknyttet + persondata for alltid. + + Skriv inn brukernavnet ditt for å fortsette. + onChangeText(text)} + value={value} + style={styles.input} + /> + {deleteUserError && } + + {isDeletingUser && } + + + ); +}; + +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, + }, +}); diff --git a/src/modules/membership/useMembership.tsx b/src/modules/membership/useMembership.tsx index 2b8114a..ff2123a 100644 --- a/src/modules/membership/useMembership.tsx +++ b/src/modules/membership/useMembership.tsx @@ -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, }; diff --git a/src/modules/membership/useUserDelete.tsx b/src/modules/membership/useUserDelete.tsx new file mode 100644 index 0000000..4db60ad --- /dev/null +++ b/src/modules/membership/useUserDelete.tsx @@ -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; diff --git a/src/reducers.ts b/src/reducers.ts index 3a31f00..5c0b3cd 100644 --- a/src/reducers.ts +++ b/src/reducers.ts @@ -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 | null; userError: Record | null; registerError: Record | null; + deleteUserError: Record | 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; } diff --git a/src/types/data.ts b/src/types/data.ts index 2de790a..7f8381c 100644 --- a/src/types/data.ts +++ b/src/types/data.ts @@ -30,4 +30,5 @@ export interface UserType { last_membership: MembershipType | null; is_volunteer: boolean; is_member: boolean; + username: string | null; } diff --git a/src/types/navigation.ts b/src/types/navigation.ts index 08c0ec4..30aab5a 100644 --- a/src/types/navigation.ts +++ b/src/types/navigation.ts @@ -14,6 +14,7 @@ export type UserStackParamList = { Membership: undefined; Login: undefined; UserRegister: undefined; + UserDelete: undefined; }; export type HomeTabParamList = {