add more options to user management

This commit is contained in:
ivic00
2024-11-12 21:38:03 +01:00
parent e2aae47c34
commit 3fb9dd0035
8 changed files with 868 additions and 577 deletions

View File

@ -0,0 +1,17 @@
import * as React from "react";
import Svg, { SvgProps, Path } from "react-native-svg";
const MenuDotsIcon = (props: SvgProps) => (
<Svg
width={props.width || 4}
height={props.height || 15}
viewBox="0 0 4 15"
fill="none"
{...props}
>
<Path
fill={props.color || "#7C7C7C"}
d="M.88 7.563a1.56 1.56 0 1 0 3.12 0 1.56 1.56 0 0 0-3.12 0Zm0-5.2A1.56 1.56 0 1 0 4 2.426a1.56 1.56 0 0 0-3.12-.063Zm0 10.4A1.56 1.56 0 1 0 4 12.701a1.56 1.56 0 0 0-3.12.062Z"
/>
</Svg>
);
export default MenuDotsIcon;

View File

@ -98,7 +98,6 @@ const DeleteProfileDialogs: React.FC<ConfirmationDialogProps> = ({
);
};
// Empty stylesheet for future styles
const styles = StyleSheet.create({
confirmBtn: {
backgroundColor: "#ff1637",

View File

@ -3,7 +3,8 @@ import {
Button,
Card,
Colors,
Dialog, Image,
Dialog,
Image,
KeyboardAwareScrollView,
PanningProvider,
Picker,
@ -13,31 +14,38 @@ import {
TouchableOpacity,
View,
} from "react-native-ui-lib";
import React, {useEffect, useRef, useState} from "react";
import {ImageBackground, Platform, StyleSheet} from "react-native";
import {PickerSingleValue} from "react-native-ui-lib/src/components/picker/types";
import {useCreateSubUser} from "@/hooks/firebase/useCreateSubUser";
import {ProfileType, useAuthContext} from "@/contexts/AuthContext";
import {useGetFamilyMembers} from "@/hooks/firebase/useGetFamilyMembers";
import React, { useEffect, useRef, useState } from "react";
import { ImageBackground, Platform, StyleSheet } from "react-native";
import { PickerSingleValue } from "react-native-ui-lib/src/components/picker/types";
import { useCreateSubUser } from "@/hooks/firebase/useCreateSubUser";
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { useGetFamilyMembers } from "@/hooks/firebase/useGetFamilyMembers";
import UserMenu from "@/components/pages/settings/user_settings_views/UserMenu";
import {uuidv4} from "@firebase/util";
import { uuidv4 } from "@firebase/util";
import QRIcon from "@/assets/svgs/QRIcon";
import EmailIcon from "@/assets/svgs/EmailIcon";
import CircledXIcon from "@/assets/svgs/CircledXIcon";
import ProfileIcon from "@/assets/svgs/ProfileIcon";
import NavToDosIcon from "@/assets/svgs/NavToDosIcon";
import Ionicons from "@expo/vector-icons/Ionicons";
import KeyboardManager, {PreviousNextView,} from "react-native-keyboard-manager";
import {ScrollView} from "react-native-gesture-handler";
import {useUploadProfilePicture} from "@/hooks/useUploadProfilePicture";
import {ImagePickerAsset} from "expo-image-picker";
import KeyboardManager, {
PreviousNextView,
} from "react-native-keyboard-manager";
import { ScrollView } from "react-native-gesture-handler";
import { useUploadProfilePicture } from "@/hooks/useUploadProfilePicture";
import { ImagePickerAsset } from "expo-image-picker";
import MenuDotsIcon from "@/assets/svgs/MenuDotsIcon";
import UserOptions from "./UserOptions";
type MyGroupProps = {
onNewUserClick: boolean;
setOnNewUserClick: React.Dispatch<React.SetStateAction<boolean>>;
};
const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) => {
const MyGroup: React.FC<MyGroupProps> = ({
onNewUserClick,
setOnNewUserClick,
}) => {
const [showAddUserDialog, setShowAddUserDialog] = useState(false);
const [selectedStatus, setSelectedStatus] = useState<
string | PickerSingleValue
@ -46,7 +54,9 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [newUserId, setNewUserId] = useState("")
const { profileData } = useAuthContext();
const [newUserId, setNewUserId] = useState("");
const lNameRef = useRef<TextFieldRef>(null);
const emailRef = useRef<TextFieldRef>(null);
@ -55,10 +65,16 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
false
);
const {mutateAsync: createSubUser, isLoading, isError} = useCreateSubUser();
const {data: familyMembers} = useGetFamilyMembers(true);
const {user} = useAuthContext();
const {pickImage, changeProfilePicture, handleClearImage, pfpUri, profileImageAsset} = useUploadProfilePicture(newUserId)
const { mutateAsync: createSubUser, isLoading, isError } = useCreateSubUser();
const { data: familyMembers } = useGetFamilyMembers(true);
const { user } = useAuthContext();
const {
pickImage,
changeProfilePicture,
handleClearImage,
pfpUri,
profileImageAsset,
} = useUploadProfilePicture(newUserId);
const parents =
familyMembers?.filter((x) => x.userType === ProfileType.PARENT) ?? [];
@ -103,7 +119,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
if (res?.data?.userId) {
if (profileImageAsset) {
await changeProfilePicture(profileImageAsset)
await changeProfilePicture(profileImageAsset);
setShowQRCodeDialog(res.data.userId);
} else {
setTimeout(() => {
@ -111,7 +127,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
}, 500);
}
handleClearImage()
handleClearImage();
}
}
};
@ -156,11 +172,11 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
<ImageBackground
style={styles.pfp}
borderRadius={10.56}
source={{uri: member.pfp || undefined}}
source={{ uri: member.pfp || undefined }}
/>
) : (
<View
style={[styles.pfp, {backgroundColor: "#ea156d"}]}
style={[styles.pfp, { backgroundColor: "#ea156d" }]}
/>
)}
<View row marginL-10 centerV>
@ -168,7 +184,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
{member.firstName} {member.lastName}
</Text>
</View>
<View flexG/>
<View flexG />
<View row centerV gap-10>
<Text style={styles.userType}>
{member.userType === ProfileType.PARENT
@ -180,6 +196,8 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
showQRCodeDialog={showQRCodeDialog === member?.uid}
user={member}
/>
{profileData?.userType === ProfileType.PARENT &&
user?.uid != member.uid && <UserOptions user={member} />}
</View>
</Card>
))}
@ -202,7 +220,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
padding-10
>
<Avatar
source={{uri: member?.pfp ?? undefined}}
source={{ uri: member?.pfp ?? undefined }}
size={40}
backgroundColor={Colors.grey60}
/>
@ -215,7 +233,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
</Text>
</View>
<View flex-1/>
<View flex-1 />
<UserMenu
setShowQRCodeDialog={(val) => setShowQRCodeDialog(val)}
@ -243,7 +261,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
padding-10
>
<Avatar
source={{uri: member?.pfp ?? undefined}}
source={{ uri: member?.pfp ?? undefined }}
size={40}
backgroundColor={Colors.grey60}
/>
@ -254,7 +272,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
</Text>
</View>
<View flex-1/>
<View flex-1 />
<UserMenu
setShowQRCodeDialog={(val) => setShowQRCodeDialog(val)}
@ -286,7 +304,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
</Text>
<Button backgroundColor={"#FD1775"} style={styles.dialogBtn}>
<QRIcon/>
<QRIcon />
<Text style={styles.dialogBtnLbl} marginL-7>
Show a QR Code
</Text>
@ -301,7 +319,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
}, 500);
}}
>
<EmailIcon/>
<EmailIcon />
<Text style={styles.dialogBtnLbl} marginL-7>
Enter email address
</Text>
@ -326,7 +344,7 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
<KeyboardAwareScrollView>
<Card padding-25 style={styles.dialogCard}>
<View row spread>
<Text style={{fontFamily: "Manrope_500Medium", fontSize: 16}}>
<Text style={{ fontFamily: "Manrope_500Medium", fontSize: 16 }}>
New User Information
</Text>
<TouchableOpacity
@ -334,35 +352,39 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
setOnNewUserClick(false);
}}
>
<CircledXIcon/>
<CircledXIcon />
</TouchableOpacity>
</View>
<View style={styles.divider} spread/>
<View style={styles.divider} spread />
<View row centerV gap-20 marginV-20>
{pfpUri ? (
<Image
height={65.54}
width={65.54}
style={{borderRadius: 25}}
source={{uri: pfpUri}}
style={{ borderRadius: 25 }}
source={{ uri: pfpUri }}
/>
) : (
<View
height={65.54}
width={65.54}
children={
<ProfileIcon color={"#d6d6d6"} width={37} height={37}/>
<ProfileIcon color={"#d6d6d6"} width={37} height={37} />
}
backgroundColor={Colors.grey60}
style={{borderRadius: 25}}
style={{ borderRadius: 25 }}
center
/>
)}
{pfpUri ? (
<TouchableOpacity onPress={handleClearImage}>
<Text color={Colors.red40} style={styles.jakarta13} marginL-15>
<Text
color={Colors.red40}
style={styles.jakarta13}
marginL-15
>
Clear user photo
</Text>
</TouchableOpacity>
@ -373,7 +395,6 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
</Text>
</TouchableOpacity>
)}
</View>
<Text style={styles.jakarta12}>Member Status</Text>
@ -397,15 +418,15 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
>
<Ionicons
name={"chevron-down"}
style={{alignSelf: "center"}}
style={{ alignSelf: "center" }}
size={20}
color={"#000000"}
/>
</View>
}
>
<Picker.Item label="Child" value={ProfileType.CHILD}/>
<Picker.Item label="Parent" value={ProfileType.PARENT}/>
<Picker.Item label="Child" value={ProfileType.CHILD} />
<Picker.Item label="Parent" value={ProfileType.PARENT} />
<Picker.Item
label="Caregiver"
value={ProfileType.CAREGIVER}
@ -488,8 +509,8 @@ const MyGroup: React.FC<MyGroupProps> = ({onNewUserClick, setOnNewUserClick}) =>
fontSize: 15,
marginLeft: 7,
}}
style={{marginTop: 20, backgroundColor: "#fd1775"}}
iconSource={() => <NavToDosIcon width={22} color={"white"}/>}
style={{ marginTop: 20, backgroundColor: "#fd1775" }}
iconSource={() => <NavToDosIcon width={22} color={"white"} />}
onPress={handleCreateSubUser}
/>
</Card>
@ -505,7 +526,7 @@ const styles = StyleSheet.create({
height: 47,
width: 279,
},
dialogTitle: {fontFamily: "Manrope_600SemiBold", fontSize: 22},
dialogTitle: { fontFamily: "Manrope_600SemiBold", fontSize: 22 },
dialogBackBtn: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 15,
@ -593,7 +614,7 @@ const styles = StyleSheet.create({
fontSize: 15,
color: "white",
},
divider: {height: 0.7, backgroundColor: "#e6e6e6", width: "100%"},
divider: { height: 0.7, backgroundColor: "#e6e6e6", width: "100%" },
jakarta12: {
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 12,
@ -603,7 +624,7 @@ const styles = StyleSheet.create({
fontFamily: "PlusJakartaSans_500Medium",
fontSize: 13,
},
pfp: {aspectRatio: 1, width: 37.03, borderRadius: 10.56},
pfp: { aspectRatio: 1, width: 37.03, borderRadius: 10.56 },
userType: {
fontFamily: "Manrope_500Medium",
fontSize: 12,

View File

@ -0,0 +1,140 @@
import { Platform, StyleSheet } from "react-native";
import React, { useState } from "react";
import { MenuView } from "@react-native-menu/menu";
import { Dialog, Text, View, Button } from "react-native-ui-lib";
import MenuDotsIcon from "@/assets/svgs/MenuDotsIcon";
import { UserProfile } from "@/hooks/firebase/types/profileTypes";
import { useRemoveSubUser } from "@/hooks/firebase/useRemoveSubUser";
const UserOptions = ({ user }: { user: UserProfile }) => {
const [visible, setVisible] = useState<boolean>(false);
const { mutateAsync: removeSubUser } = useRemoveSubUser();
const handleDeleteUser = async () => {
try {
if (user.uid) await removeSubUser(user.uid);
} catch (error) {
console.error(error);
} finally {
setVisible(false);
}
};
const menuOptions = [
{
id: "edit",
title: "Edit User",
onPress: () => console.log("Edit User here"),
image: Platform.select({
ios: "pencil.and.ellipsis.rectangle",
android: "ic_menu_edit",
}),
imageColor: "#7300d4",
titleColor: "#7300d4",
},
{
id: "delete",
title: "Delete User",
attributes: {
destructive: true,
},
image: Platform.select({
ios: "trash",
android: "ic_menu_delete",
}),
},
];
return (
<>
<MenuView
style={styles.menu}
title="User options"
onPressAction={({ nativeEvent }) => {
switch (nativeEvent.event) {
case "edit":
console.log("Edit User here");
break;
case "delete":
setTimeout(() => setVisible(true), 300);
break;
default:
break;
}
}}
actions={menuOptions}
shouldOpenOnLongPress={false}
>
<MenuDotsIcon />
</MenuView>
<Dialog
visible={visible}
containerStyle={styles.dialog}
onDismiss={() => setVisible(false)}
>
<Text center style={styles.title}>
Are you sure?
</Text>
<Text
style={{
fontSize: 18,
fontFamily: "PlusJakartaSans_700Bold",
color: "#979797",
marginBottom: 20,
}}
center
>
This action will delete the {user.firstName}'s profile.
</Text>
<View row right gap-8 marginT-15>
<Button
label="Cancel"
onPress={() => {
setVisible(false);
}}
style={styles.cancelBtn}
color="#999999"
labelStyle={{ fontFamily: "Poppins_500Medium", fontSize: 13.53 }}
/>
<Button
label="Yes"
onPress={handleDeleteUser}
style={styles.confirmBtn}
labelStyle={{ fontFamily: "PlusJakartaSans_500Medium" }}
/>
</View>
</Dialog>
</>
);
};
const styles = StyleSheet.create({
menu: {
padding: 5,
},
confirmBtn: {
backgroundColor: "#ff1637",
},
cancelBtn: {
backgroundColor: "white",
},
dialog: {
backgroundColor: "white",
paddingHorizontal: 25,
paddingTop: 25,
paddingBottom: 17,
borderRadius: 20,
},
title: {
fontFamily: "Manrope_600SemiBold",
fontSize: 22,
marginBottom: 5,
},
text: {
fontFamily: "PlusJakartaSans_400Regular",
fontSize: 16,
marginBottom: 0,
},
});
export default UserOptions;

View File

@ -162,6 +162,87 @@ exports.createSubUser = onRequest(async (request, response) => {
}
});
exports.removeSubUser = onRequest(async (request, response) => {
const authHeader = request.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
logger.warn("Missing or incorrect Authorization header", {authHeader});
response.status(401).json({error: 'Unauthorized'});
return;
}
try {
const token = authHeader.split('Bearer ')[1];
logger.info("Verifying ID token", {token});
let decodedToken;
try {
decodedToken = await getAuth().verifyIdToken(token);
logger.info("ID token verified successfully", {uid: decodedToken.uid});
} catch (verifyError) {
logger.error("ID token verification failed", {error: verifyError.message});
response.status(401).json({error: 'Unauthorized: Invalid token'});
return;
}
logger.info("Processing user removal", {requestBody: request.body.data});
const { userId, familyId } = request.body.data;
if (!userId || !familyId) {
logger.warn("Missing required fields in request body", {requestBody: request.body.data});
response.status(400).json({error: "Missing required fields"});
return;
}
try {
const userProfile = await getFirestore()
.collection("Profiles")
.doc(userId)
.get();
if (!userProfile.exists) {
logger.error("User profile not found", {userId});
response.status(404).json({error: "User not found"});
return;
}
if (userProfile.data().familyId !== familyId) {
logger.error("User does not belong to the specified family", {
userId,
requestedFamilyId: familyId,
actualFamilyId: userProfile.data().familyId
});
response.status(403).json({error: "User does not belong to the specified family"});
return;
}
await getFirestore()
.collection("Profiles")
.doc(userId)
.delete();
logger.info("User profile deleted from Firestore", {userId});
await getAuth().deleteUser(userId);
logger.info("User authentication deleted", {userId});
response.status(200).json({
data: {
message: "User removed successfully",
success: true
}
});
} catch (error) {
logger.error("Failed to remove user", {error: error.message});
response.status(500).json({error: "Failed to remove user"});
return;
}
} catch (error) {
logger.error("Error in removeSubUser function", {error: error.message});
response.status(500).json({data: {error: error.message}});
}
});
exports.generateCustomToken = onRequest(async (request, response) => {
try {
const {userId} = request.body.data;

View File

@ -0,0 +1,27 @@
import { useMutation, useQueryClient } from "react-query";
import functions from '@react-native-firebase/functions';
import { ProfileType, useAuthContext } from "@/contexts/AuthContext";
import { HttpsCallableResult } from "@firebase/functions";
export const useRemoveSubUser = () => {
const queryClient = useQueryClient();
const { profileType, profileData } = useAuthContext();
return useMutation({
mutationKey: ["removeSubUser"],
mutationFn: async (userId: string) => {
if (profileType === ProfileType.PARENT) {
return await functions().httpsCallable("removeSubUser")({
userId,
familyId: profileData?.familyId!,
}) as HttpsCallableResult<{ success: boolean }>;
} else {
throw Error("Can't remove sub-users as a non-parent.");
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["getChildrenByParentId"] });
queryClient.invalidateQueries({ queryKey: ["familyMembers"] });
},
});
};

View File

@ -40,6 +40,7 @@
"@react-native-firebase/firestore": "^20.4.0",
"@react-native-firebase/functions": "^20.4.0",
"@react-native-firebase/storage": "^21.0.0",
"@react-native-menu/menu": "^1.1.6",
"@react-navigation/drawer": "^6.7.2",
"@react-navigation/native": "^6.0.2",
"date-fns": "^3.6.0",

View File

@ -2573,6 +2573,11 @@
resolved "https://registry.npmjs.org/@react-native-firebase/storage/-/storage-21.0.0.tgz"
integrity sha512-meft5Pu0nI7zxhpnP49ko9Uw8GaIy9hXGJfa/fCFrpf2vA9OXdTr3CvgloH/b9DpbkwQGcGTshRqltuttXI67w==
"@react-native-menu/menu@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@react-native-menu/menu/-/menu-1.1.6.tgz#df6b4bf46a8ac5718605203f7fcd6fd3684715f5"
integrity sha512-KRPBqa9jmYDFoacUxw8z1ucpbvmdlPuRO8tsFt2jM8JMC2s+YQwTtISG73PeqH9KD7BV+8igD/nizPfcipOmhQ==
"@react-native/assets-registry@0.74.85":
version "0.74.85"
resolved "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.74.85.tgz"