Repository: ISPP-2425-G9/frontend
Pull request #83: Fr 016 load certificate
Author: login: rafduqcol
Created: 2025-03-20T09:18:43Z | Merged: 2025-03-20T16:25:39Z

** Description **

- Added load certificate without account
- Added load certificate with account
- Create a obituary with a certificate
- Fix some bugs of obituaries that involve the certificate


Diff:
diff --git a/app/_layout.tsx b/app/_layout.tsx
index f80b216d..976b3047 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -30,6 +30,7 @@ export default function TabLayout() {
         tabBarIcon: ({ color, size, focused }) => {
           const iconsDict: Record<string, keyof typeof Ionicons.glyphMap> = {
             "profile/index": "person",
+            "certificate/index": "newspaper-outline",
             "obituaries/index": "document",
             "messages/index": "chatbox",
             "contacts/index": "book-sharp",
@@ -88,13 +89,19 @@ export default function TabLayout() {
       <Tabs.Screen name="index" options={{ href: null }} />
       <Tabs.Screen name="+not-found" options={{ href: null }} />
 
+
       {
         isAuthenticated ? [
+          <Tabs.Screen name="certificate/index" options={{ title: "Cargar certificado" }} />,
           <Tabs.Screen name="login/index" options={{ href: null }} />,
           <Tabs.Screen name="register/index" options={{ href: null }} />,
         ] : [
+          <Tabs.Screen name="certificate/index" options={{ title: "Cargar certificado" }} />,
           <Tabs.Screen name="login/index" options={{ title: "" }} />,
           <Tabs.Screen name="register/index" options={{ title: "" }} />,
+          <Tabs.Screen name="obituaries/loadCertificate" options={{ href: null }} />,
+
+
         ]
       }
 
@@ -120,7 +127,6 @@ export default function TabLayout() {
           <Tabs.Screen name="obituaries/createObituary" options={{ href: null }} />,
           <Tabs.Screen name="obituaries/listMyObituaries" options={{ href: null }} />,
           <Tabs.Screen name="obituaries/selectContacts" options={{ href: null }} />,
-          <Tabs.Screen name="obituaries/loadCertificate" options={{ href: null }} />,
         ]
       }
 
diff --git a/app/certificate/index.tsx b/app/certificate/index.tsx
new file mode 100644
index 00000000..b7e9db2b
--- /dev/null
+++ b/app/certificate/index.tsx
@@ -0,0 +1,348 @@
+import { useState } from "react";
+import { View, Text, TextInput, Button, StyleSheet, Image } from "react-native";
+import { useNavigation, NavigationProp, useRoute, RouteProp } from "@react-navigation/native";
+import { Dimensions } from "react-native";
+import { TouchableOpacity } from "react-native";
+import { AUTHORITIES } from "../_util/Authorities";
+import { withAuth } from "../_util/withAuth";
+import * as ImagePicker from "expo-image-picker";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import CustomButton from "@/components/CustomButton";
+import { CustomTextInput } from "@/components/CustomTextInput";
+import { GlobalStyles } from "@/constants/Colors";
+import { ThemedView } from "@/components/ThemedView";
+import CustomModal from "@/components/CustomModal";
+import { BACKEND_API } from "@/constants/Mysc";
+
+type RootStackParamList = {
+  "obituaries/loadCertificate": { jsonData: string },
+  "home": undefined,
+};
+
+
+const { width } = Dimensions.get("window");
+
+function LoadCertificate() {
+  const navigation = useNavigation<NavigationProp<RootStackParamList>>();
+  const [formData, setFormData] = useState({
+    dni: "",
+    certificateImage: "",
+  });
+
+  const [dni, setDni] = useState<string>("");
+  const [certificateImage, setCertificateImage] = useState<string | null>(null);
+  const [fileName, setFileName] = useState<string | null>(null);
+  const [dniError, setDniError] = useState<string>("");
+  const [modalVisible, setModalVisible] = useState(false);
+  const [modalMessage, setModalMessage] = useState("");
+
+  const pickImage = async () => {
+    let result = await ImagePicker.launchImageLibraryAsync({
+      mediaTypes: ImagePicker.MediaTypeOptions.All,
+      allowsEditing: true,
+      quality: 1,
+    });
+
+    if (!result.canceled) {
+      const fileUri = result.assets[0].uri;
+      const fileName = result.assets[0].fileName || null;
+      setFormData({ ...formData, certificateImage: fileUri });
+      setCertificateImage(fileUri);
+      setFileName(fileName);
+    }
+  };
+
+  const convertToBase64 = async (uri: string) => {
+    const response = await fetch(uri);
+    const blob = await response.blob();
+
+    return new Promise<string>((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onloadend = () => {
+        const base64Data = reader.result as string;
+        resolve(base64Data);
+      };
+      reader.onerror = reject;
+      reader.readAsDataURL(blob);
+    });
+  };
+
+  const validateDni = (dni: string) => {
+    const dniRegex = /^\d{8}[A-Z]$/;
+    return dniRegex.test(dni);
+  };
+
+
+  const showConfirmationModal = async () => {
+    if (!dni || !certificateImage) {
+      alert("Por favor, introduce el DNI y selecciona un archivo.");
+      return;
+    }
+    if (!validateDni(dni)) {
+      setDni("");
+      setDniError("El DNI no es válido. Debe tener el formato 12345678A.");
+      return;
+    }
+    setModalMessage("La esquela no será enviada hasta que un administrador del sistema verifique que el certificado sea válido. Podrá modificar su esquela hasta que sea enviada a todos los contactos que usted eligió.")
+    setModalVisible(true);
+  };
+
+  const handleCloseModal = () => {
+    setModalVisible(false);
+  };
+
+
+  const handleSubmit = async () => {
+
+    const base64File = certificateImage ? await convertToBase64(certificateImage) : "";
+    const authToken = await AsyncStorage.getItem("authToken");
+    console.log("authToken:", authToken);
+    const dataToSend = {
+      dni,
+      file: base64File,
+    };
+
+    if (authToken !== null) {
+      try {
+        const response = await fetch(BACKEND_API + '/api/deathCertificate/upload/loggedInUser', {
+          method: "POST",
+          headers: {
+            "Content-Type": "application/json",
+            "Authorization": `Bearer ${authToken}`,
+          },
+          body: JSON.stringify(dataToSend),
+        });
+
+        if (!response.ok) {
+          const errorData = await response.json();
+          setModalVisible(false);
+          await new Promise((resolve) => setTimeout(resolve, 200));
+
+          throw new Error(errorData.error || "Hubo un problema al enviar los datos. Inténtalo de nuevo.");
+        }
+        setModalVisible(false);
+        navigation.navigate("home");
+
+
+      } catch (error) {
+        console.error("Error al enviar datos:", error);
+        const errorMessage = (error as Error).message || "Hubo un problema al enviar los datos. Inténtalo de nuevo.";
+        setModalVisible(false);
+        alert(errorMessage);
+      }
+
+    } else {
+      try {
+        const response = await fetch(BACKEND_API + '/api/deathCertificate/upload', {
+          method: "POST",
+          headers: {
+            "Content-Type": "application/json",
+          },
+          body: JSON.stringify(dataToSend),
+        });
+
+        if (!response.ok) {
+          const errorData = await response.json();
+          setModalVisible(false);
+          // Asegurar que el modal se pong aen false
+          await new Promise((resolve) => setTimeout(resolve, 200));
+
+          throw new Error(errorData.error || "Hubo un problema al enviar los datos. Inténtalo de nuevo.");
+        }
+
+        setModalVisible(false);
+        navigation.navigate("home");
+
+      } catch (error) {
+        console.error("Error al enviar datos:", error);
+        const errorMessage = (error as Error).message || "Hubo un problema al enviar los datos. Inténtalo de nuevo.";
+        setModalVisible(false);
+        alert(errorMessage);
+      }
+    }
+
+  }
+
+
+
+
+
+  return (
+    <View style={styles.container}>
+      <View style={styles.dataContainer}>
+        <Text style={styles.title}>Carga el certificado de defunción</Text>
+
+        <Text>DNI:</Text>
+        <CustomTextInput
+          placeholder={dniError ? dniError : "Dni del fallecido"}
+          value={dni}
+          maxLength={9}
+          keyboardType="numeric"
+          onChangeText={(value) => {
+            let newValue = value.replace(/[^0-9A-Za-z]/g, "");
+
+            if (newValue.length > 9) {
+              newValue = newValue.slice(0, 9);
+            }
+
+            if (newValue.length <= 8) {
+              newValue = newValue.replace(/[^0-9]/g, "");
+            }
+
+            if (newValue.length === 9) {
+              const lastChar = newValue[8];
+              if (!/[A-Za-z]/.test(lastChar)) {
+                newValue = newValue.slice(0, 8);
+              } else {
+                newValue = newValue.slice(0, 8) + lastChar.toUpperCase();
+              }
+            }
+            setDni(newValue);
+            setDniError("");
+          }}
+          style={styles.input}
+          placeholderTextColor={dniError ? "red" : GlobalStyles.darkGrey}
+        />
+
+        {certificateImage && (
+          <Image
+            source={{ uri: certificateImage }}
+            style={styles.imagePreview}
+          />
+        )}
+
+        {fileName && (
+          <Text style={styles.fileNameText}>
+            Archivo subido: {fileName}
+          </Text>
+        )}
+
+      </View>
+      <View style={styles.divider} />
+      <View style={styles.buttonContainer}>
+        <CustomButton title="Seleccionar archivo" onPress={pickImage} />
+        <CustomButton title="Subir certificado" onPress={showConfirmationModal} />
+      </View>
+      {modalVisible && (
+        <CustomModal
+          visible={modalVisible}
+          onClose={handleCloseModal}
+          title={modalMessage}
+          style={styles.modalStyle}
+        >
+          <View style={styles.buttonContainer}>
+            <TouchableOpacity
+              style={styles.button}
+              onPress={() => handleSubmit()}
+            >
+              <Text style={styles.buttonText}>Aceptar</Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={styles.button}
+              onPress={() => { handleCloseModal() }}
+            >
+              <Text style={styles.buttonText}>Cancelar</Text>
+            </TouchableOpacity>
+          </View>
+        </CustomModal>
+      )}
+
+    </View>
+
+
+  );
+}
+
+const styles = StyleSheet.create({
+  container: {
+    padding: 8,
+    flex: 1,
+    alignItems: "center",
+    justifyContent: "flex-start",
+    backgroundColor: "#ffff",
+  },
+  dataContainer: {
+    flex: 1,
+    justifyContent: "flex-start",
+    alignItems: "center",
+    paddingTop: 120,
+  },
+  infoText: {
+    fontSize: 14,
+    color: GlobalStyles.white,
+    textAlign: "center",
+    marginTop: 10,
+    paddingVertical: 10,
+    paddingHorizontal: 15,
+  },
+  title: {
+    fontSize: 22,
+    fontWeight: "bold",
+    marginBottom: 10,
+  },
+  input: {
+    width: "100%",
+    height: 40,
+    borderWidth: 1,
+    borderColor: "#ccc",
+    paddingHorizontal: 8,
+    marginBottom: 16,
+    fontSize: 12,
+  },
+  divider: {
+    height: 1,
+    width: "100%",
+    backgroundColor: "#ccc",
+    marginVertical: 20,
+  },
+  buttonContainer: {
+    alignItems: "center",
+    justifyContent: "center",
+    marginBottom: "1%",
+    flexDirection: "row",
+    width: "35%",
+    gap: "2%",
+  },
+  fileNameText: {
+    marginTop: 10,
+    fontSize: 14,
+    fontWeight: "bold",
+  },
+  imagePreview: {
+    width: "30%",
+    height: "30%",
+    marginTop: 10,
+    resizeMode: "contain"
+  },
+  errorText: {
+    fontSize: 8,
+    color: "red",
+    marginTop: 5,
+  },
+  buttonText: {
+    color: "#fff",
+    fontSize: 16,
+    fontWeight: "bold",
+    textAlign: "center",
+  },
+  button: {
+    backgroundColor: GlobalStyles.blue,
+    paddingVertical: 12,
+    paddingHorizontal: 25,
+    borderRadius: 8,
+    alignItems: "center",
+  },
+  modalStyle: {
+    backgroundColor: "#fff",
+    padding: 20,
+    borderRadius: 15,
+    alignItems: "center",
+    shadowColor: "#000",
+    shadowOpacity: 0.2,
+    shadowRadius: 5,
+    elevation: 5,
+    width: width > 600 ? "40%" : "80%",
+  },
+});
+
+export default withAuth(LoadCertificate, [AUTHORITIES.CUSTOMER, AUTHORITIES.ANONYMOUS]);
diff --git a/app/obituaries/createObituary.tsx b/app/obituaries/createObituary.tsx
index 007e491a..31e6f315 100644
--- a/app/obituaries/createObituary.tsx
+++ b/app/obituaries/createObituary.tsx
@@ -39,6 +39,7 @@ type RootStackParamList = {
     jsonData: string;
     is_newObituary: boolean;
     obituaryId: number;
+    is_mine: boolean | undefined;
   };
   "obituaries/createObituary": {
     imageTemplateId: number;
@@ -78,6 +79,7 @@ function EsquelaCustomizer() {
 
   const jsonData = route.params?.jsonData ?? undefined;
 
+  const [ is_mine, setIsMine ] = useState();
 
   const [formData, setFormData] = useState({
     name: "",
@@ -142,8 +144,16 @@ function EsquelaCustomizer() {
           if (!response.ok) throw new Error("Error al obtener los datos");
           const data = await response.json();
 
-          const [year, month, day] = data.birthDate.split("-");
-          const formatBirthDate = `${day}/${month}/${year}`;
+          setIsMine(data.isMine);
+          console.log(is_mine, "data");
+
+          let formatBirthDate = "";  
+          if (data.birthDate) {
+            const [year, month, day] = data.birthDate.split("-") || [];
+            if (day && month && year) {
+              formatBirthDate = `${day}/${month}/${year}`;
+              }
+          }
 
           setFormData({
             name: data.name || "",
@@ -257,12 +267,14 @@ function EsquelaCustomizer() {
   };
 
   const handleSubmit = () => {
+    console.log(is_mine, "is_mine");
     if (validateForm()) {
       const jsonData = JSON.stringify(formData, null, 2);
       navigation.navigate("obituaries/selectContacts" as never, {
         jsonData: jsonData,
         is_newObituary,
         obituaryId,
+        is_mine: is_mine !== undefined ? is_mine : undefined, 
       });
     }
     setModalVisible(false);
diff --git a/app/obituaries/loadCertificate.tsx b/app/obituaries/loadCertificate.tsx
index d2be15ab..d8ddb24d 100644
--- a/app/obituaries/loadCertificate.tsx
+++ b/app/obituaries/loadCertificate.tsx
@@ -1,20 +1,379 @@
-import { useNavigation, NavigationProp, useRoute, RouteProp } from '@react-navigation/native';
-import { AUTHORITIES } from '../_util/Authorities';
-import { withAuth } from '../_util/withAuth';
+import { useState, useEffect } from "react";
+import { View, Text, TextInput, Button, StyleSheet, Image } from "react-native";
+import { useNavigation, NavigationProp, useRoute, RouteProp } from "@react-navigation/native";
+import { Dimensions } from "react-native";
+import { TouchableOpacity } from "react-native";
+import { AUTHORITIES } from "../_util/Authorities";
+import { withAuth } from "../_util/withAuth";
+import * as ImagePicker from "expo-image-picker";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import CustomButton from "@/components/CustomButton";
+import { CustomTextInput } from "@/components/CustomTextInput";
+import { GlobalStyles } from "@/constants/Colors";
+import { ThemedView } from "@/components/ThemedView";
+import useAuth from "@/hooks/useAuth";
+import CustomModal from "@/components/CustomModal";
+import { BACKEND_API } from "@/constants/Mysc";
 
 type RootStackParamList = {
-    'obituaries/loadCertificate': { jsonData: string };
+  "obituaries/loadCertificate": {
+    jsonData: string
+    is_newObituary: boolean,
+    obituaryId: string,
+    isMine: boolean
+  };
+  "obituaries/index": undefined;
 };
 
+type ObituaryLoadCertificateRouteProp = RouteProp<
+  RootStackParamList,
+  "obituaries/loadCertificate"
+>;
+
+const { width } = Dimensions.get("window");
+
 function LoadCertificate() {
-    const navigation = useNavigation<NavigationProp<RootStackParamList>>();
-    const route = useRoute<RouteProp<RootStackParamList, 'obituaries/loadCertificate'>>();
-
-    return (
-        <div>
-            Aquí se cargará el certificado de defunción
-        </div>
-    );
+  const navigation = useNavigation<NavigationProp<RootStackParamList>>();
+
+
+  const route = useRoute<ObituaryLoadCertificateRouteProp>();
+  const is_newObituary = route.params?.is_newObituary;
+
+
+  const { isAuthenticated } = useAuth();
+  const [dni, setDni] = useState<string>("");
+  const [certificateImage, setCertificateImage] = useState<string | null>(null);
+  const [fileName, setFileName] = useState<string | null>(null);
+  const [dniError, setDniError] = useState<string>("");
+  const [modalVisible, setModalVisible] = useState(false);
+  const [modalMessage, setModalMessage] = useState("");
+  const [combinedData, setCombinedData] = useState<unknown>({});
+
+  const isMine = route.params?.isMine;
+  const [formData, setFormData] = useState({
+    dni: "",
+    certificateImage: "",
+  });
+
+
+  useEffect(() => {
+    const initializeForm = async () => {
+      if (is_newObituary === true) {
+        setFormData({
+          dni: "",
+          certificateImage: "",
+        });
+        return;
+      } else {
+
+        const authToken = await AsyncStorage.getItem("authToken");
+        const obituaryId = route.params?.obituaryId ?? "";
+        const response = await fetch(`${BACKEND_API}/api/deathCertificate/obituary/${obituaryId}`, {
+          method: "GET",
+          headers: {
+            "Content-Type": "application/json",
+            Authorization: `Bearer ${authToken}`,
+          },
+        }
+        );
+        if (!response.ok) throw new Error("Error al obtener los datos");
+        const data = await response.json();
+        setDni(data.dni);
+        setCertificateImage(data.deathCertificate.url);
+      }
+      console.log("Datos del formulario:", certificateImage);
+    };
+
+    void initializeForm();
+  }, [is_newObituary]);
+
+
+  const pickImage = async () => {
+    let result = await ImagePicker.launchImageLibraryAsync({
+      mediaTypes: ImagePicker.MediaTypeOptions.All,
+      allowsEditing: true,
+      quality: 1,
+    });
+
+    if (!result.canceled) {
+      const fileUri = result.assets[0].uri;
+      const fileName = result.assets[0].fileName || null;
+      setFormData({ ...formData, certificateImage: fileUri });
+      setCertificateImage(fileUri);
+      setFileName(fileName);
+    }
+  };
+
+  const convertToBase64 = async (uri: string) => {
+    const response = await fetch(uri);
+    const blob = await response.blob();
+
+    return new Promise<string>((resolve, reject) => {
+      const reader = new FileReader();
+      reader.onloadend = () => {
+        const base64Data = reader.result as string;
+        resolve(base64Data);
+      };
+      reader.onerror = reject;
+      reader.readAsDataURL(blob);
+    });
+  };
+
+  const validateDni = (dni: string) => {
+    const dniRegex = /^\d{8}[A-Z]$/;
+    return dniRegex.test(dni);
+  };
+
+
+  const showConfirmationModal = async () => {
+    if (!dni || !certificateImage) {
+      alert("Por favor, introduce el DNI y selecciona un archivo.");
+      return;
+    }
+    if (!validateDni(dni)) {
+      setDni("");
+      setDniError("El DNI debe tener el formato 12345678A.");
+      return;
+    }
+    setModalMessage("La esquela no será enviada hasta que un administrador del sistema verifique que el certificado sea válido. Podrá modificar su esquela hasta que se hayan enviado a los contactos que eligió.")
+    setModalVisible(true);
+  };
+
+  const handleCloseModal = () => {
+    setModalVisible(false);
+  };
+
+
+  const handleSubmit = async () => {
+
+    const authToken = await AsyncStorage.getItem("authToken");
+    const jsonData = route.params.jsonData ?? '';
+    const base64File = certificateImage ? await convertToBase64(certificateImage) : "";
+
+    const dataToSend = {
+      ...JSON.parse(jsonData),
+      deathCertificate: {
+        dni: dni,
+        file: base64File
+      },
+      isMine
+    };
+
+    try {
+      const response = await fetch(BACKEND_API + '/api/obituary/create', {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+          Authorization: `Bearer ${authToken}`,
+        },
+        body: JSON.stringify(dataToSend),
+      });
+
+      if (!response.ok) {
+        const errorData = await response.json();
+        throw new Error(errorData.error || "Hubo un problema al enviar los datos. Inténtalo de nuevo.");
+      }
+      setModalVisible(false);
+      navigation.navigate("obituaries/index");
+
+
+    } catch (error) {
+      console.error("Error al enviar datos:", error);
+      const errorMessage = (error as Error).message || "Hubo un problema al enviar los datos. Inténtalo de nuevo.";
+      alert(errorMessage);
+    }
+  };
+
+  return isAuthenticated ? (
+    <View style={styles.container}>
+      <View style={styles.dataContainer}>
+        <Text style={styles.title}>Carga el certificado de defunción</Text>
+
+        <Text>DNI:</Text>
+        <CustomTextInput
+          placeholder={dniError ? dniError : "Dni del fallecido"}
+          value={dni}
+          maxLength={9}
+          keyboardType="numeric"
+          onChangeText={(value) => {
+            let newValue = value.replace(/[^0-9A-Za-z]/g, "");
+
+            if (newValue.length > 9) {
+              newValue = newValue.slice(0, 9);
+            }
+
+            if (newValue.length <= 8) {
+              newValue = newValue.replace(/[^0-9]/g, "");
+            }
+
+            if (newValue.length === 9) {
+              const lastChar = newValue[8];
+              if (!/[A-Za-z]/.test(lastChar)) {
+                newValue = newValue.slice(0, 8);
+              } else {
+                newValue = newValue.slice(0, 8) + lastChar.toUpperCase();
+              }
+            }
+            setDni(newValue);
+            setDniError("");
+          }}
+          style={styles.input}
+          placeholderTextColor={dniError ? "red" : GlobalStyles.darkGrey}
+        />
+
+        {certificateImage && (
+          <Image
+            source={{ uri: certificateImage }}
+            style={styles.imagePreview}
+          />
+        )}
+
+        {fileName && (
+          <Text style={styles.fileNameText}>
+            Archivo subido: {fileName}
+          </Text>
+        )}
+
+      </View>
+      <View style={styles.divider} />
+      <View style={styles.buttonContainer}>
+        <CustomButton title="Seleccionar archivo" onPress={pickImage} />
+        <CustomButton
+          title={is_newObituary ? "Pagar esquela (1,99 €)" : "Actualizar esquela"}
+          onPress={() => {
+            if (is_newObituary) {
+              void showConfirmationModal();
+            } else {
+              window.alert("Función todavía no implementada");
+            }
+          }}
+        />
+      </View>
+      {modalVisible && (
+        <CustomModal
+          visible={modalVisible}
+          onClose={handleCloseModal}
+          title={modalMessage}
+          style={styles.modalStyle}
+        >
+          <View style={styles.buttonContainer}>
+            <TouchableOpacity
+              style={styles.button}
+              onPress={() => handleSubmit()}
+            >
+              <Text style={styles.buttonText}>Aceptar</Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={styles.button}
+              onPress={() => { handleCloseModal() }}
+            >
+              <Text style={styles.buttonText}>Cancelar</Text>
+            </TouchableOpacity>
+          </View>
+        </CustomModal>
+      )}
+
+    </View>
+
+
+  ) : (
+    <ThemedView style={styles.container}>
+      <Text style={styles.title}>Debes iniciar sesión para poder acceder a esta sección</Text>
+    </ThemedView>
+  );
 }
 
-export default withAuth(LoadCertificate, [AUTHORITIES.CUSTOMER])
\ No newline at end of file
+const styles = StyleSheet.create({
+  container: {
+    padding: 8,
+    flex: 1,
+    alignItems: "center",
+    justifyContent: "flex-start",
+    backgroundColor: "#ffff",
+  },
+  dataContainer: {
+    flex: 1,
+    justifyContent: "flex-start",
+    alignItems: "center",
+    paddingTop: 120,
+  },
+  infoText: {
+    fontSize: 14,
+    color: GlobalStyles.white,
+    textAlign: "center",
+    marginTop: 10,
+    paddingVertical: 10,
+    paddingHorizontal: 15,
+  },
+  title: {
+    fontSize: 22,
+    fontWeight: "bold",
+    marginBottom: 10,
+  },
+  input: {
+    width: "100%",
+    height: 40,
+    borderWidth: 1,
+    borderColor: "#ccc",
+    paddingHorizontal: 8,
+    marginBottom: 16,
+    fontSize: 16,
+  },
+  divider: {
+    height: 1,
+    width: "100%",
+    backgroundColor: "#ccc",
+    marginVertical: 20,
+  },
+  buttonContainer: {
+    alignItems: "center",
+    justifyContent: "center",
+    marginBottom: "1%",
+    flexDirection: "row",
+    width: "35%",
+    gap: "2%",
+  },
+  fileNameText: {
+    marginTop: 10,
+    fontSize: 14,
+    fontWeight: "bold",
+  },
+  imagePreview: {
+    width: "30%",
+    height: "30%",
+    marginTop: 10,
+    resizeMode: "contain"
+  },
+  errorText: {
+    fontSize: 8,
+    color: "red",
+    marginTop: 5,
+  },
+  buttonText: {
+    color: "#fff",
+    fontSize: 16,
+    fontWeight: "bold",
+    textAlign: "center",
+  },
+  button: {
+    backgroundColor: GlobalStyles.blue,
+    paddingVertical: 12,
+    paddingHorizontal: 25,
+    borderRadius: 8,
+    alignItems: "center",
+  },
+  modalStyle: {
+    backgroundColor: "#fff",
+    padding: 20,
+    borderRadius: 15,
+    alignItems: "center",
+    shadowColor: "#000",
+    shadowOpacity: 0.2,
+    shadowRadius: 5,
+    elevation: 5,
+    width: width > 600 ? "40%" : "80%",
+  },
+});
+
+export default withAuth(LoadCertificate, [AUTHORITIES.CUSTOMER]);
+
diff --git a/app/obituaries/selectContacts.tsx b/app/obituaries/selectContacts.tsx
index 6560a4f1..673ce3cd 100644
--- a/app/obituaries/selectContacts.tsx
+++ b/app/obituaries/selectContacts.tsx
@@ -13,7 +13,7 @@ import CustomButton from "@/components/CustomButton";
 import { CustomTextInput } from "@/components/CustomTextInput";
 import { useRoute, RouteProp } from "@react-navigation/native";
 import AsyncStorage from "@react-native-async-storage/async-storage";
-import { useNavigation } from "@react-navigation/native";
+import { useNavigation, NavigationProp  } from "@react-navigation/native";
 import CustomModal from "@/components/CustomModal";
 import { GlobalStyles } from "@/constants/Colors";
 import { BACKEND_API } from "@/constants/Mysc";
@@ -30,12 +30,18 @@ const { width } = Dimensions.get("window");
 
 type RootStackParamList = {
   "obituaries/selectContacts": {
-    jsonData: string;
-    is_newObituary: boolean;
-    obituaryId: number;
+    jsonData: string,
+    is_newObituary: boolean,
+    obituaryId: number,
+    is_mine: boolean
   };
   "obituaries/listMyObituaries": undefined;
-  "obituaries/loadCertificate": { jsonData: string };
+  "obituaries/loadCertificate": { 
+    jsonData: string, 
+    is_newObituary: boolean, 
+    obituaryId: number,
+    isMine: boolean
+  };
 };
 
 type SelectContactsRouteProp = RouteProp<
@@ -52,20 +58,15 @@ type Contact = {
 
 function SelectContacts() {
   const { isAuthenticated } = useAuth();
-  const navigation = useNavigation();
+  const navigation = useNavigation<NavigationProp<RootStackParamList>>();
   const route = useRoute<SelectContactsRouteProp>();
-  const jsonData = route.params?.jsonData ?? '';
-  if (!route.params || !route.params.jsonData) {
-    return (
-      <View style={{ padding: 30 }}>
-        <Text>Error: No se proporcionaron los datos necesarios para continuar.</Text>
-      </View>
-    );
-  }
 
+  const jsonData = route.params?.jsonData ?? "";
+  const is_newObituary = route.params?.is_newObituary;
+  const obituaryId = route.params?.obituaryId;
+  const is_mine = route.params?.is_mine;
 
-  const is_newObituary = route.params?.is_newObituary ?? true;
-  const obituaryId = route.params?.obituaryId ?? undefined;
+  const [isMine, setIsMine] = useState(false);
 
   const [modalVisible, setModalVisible] = useState(false);
   const [modalMessage, setModalMessage] = useState("");
@@ -73,9 +74,13 @@ function SelectContacts() {
     { id: Date.now(), name: "", phone: "", email: "" },
   ]);
   const [combinedData, setCombinedData] = useState<any>({});
-
   const [userRole, setUserRole] = useState<string | null>(null);
 
+  const hasError =
+    jsonData === "" ||
+    is_newObituary === undefined ||
+    obituaryId === undefined ||
+    is_mine === undefined;
   useFocusEffect(
     useCallback(() => {
       if (is_newObituary) {
@@ -200,51 +205,10 @@ function SelectContacts() {
     }
   };
 
-  const createObituary = async () => {
-    const contactsWithoutIds = contacts.map(({ id, ...rest }) => rest);
-    const dataToSend = {
-      ...combinedData,
-      contacts: contactsWithoutIds,
-      isMine: false,
-    };
-
-    const url = is_newObituary
-      ? BACKEND_API + `/api/obituary/create`
-      : BACKEND_API + `/api/obituary/update/${obituaryId}`;
-
-    const method_type = is_newObituary ? "POST" : "PUT";
-
-    try {
-
-      const authToken = await AsyncStorage.getItem("authToken");
-      const response = await fetch(url, {
-        method: method_type,
-        headers: {
-          "Content-Type": "application/json",
-          Authorization: `Bearer ${authToken}`,
-        },
-        body: JSON.stringify(dataToSend),
-      });
-
-      if (response.ok) {
-        navigation.navigate("obituaries/listMyObituaries" as never);
-      } else {
-        throw new Error("Error en la creación de la esquela");
-      }
-    } catch (error) {
-      const errormssg = is_newObituary
-        ? "Error al crear la esquela.Por favor, inténtelo de nuevo."
-        : "Error al actualizar la esquela.Por favor, inténtelo de nuevo.";
-      window.alert(errormssg);
-    }
-  };
-
-  const moveToNextScreen = () => {
-    navigation.navigate("obituaries/loadCertificate" as never);
-  };
 
-  const showConfirmationModal = async (is_mine: boolean) => {
+  const showConfirmationModal = async (isMine: boolean) => {
     const errors: string[] = [];
+    setIsMine(isMine);
 
     const phoneSet = new Set();
     const emailSet = new Set();
@@ -273,7 +237,7 @@ function SelectContacts() {
         throw new Error(`Hay error(es) en su formulario: ${errors.join(", ")}`);
       }
 
-      if (is_mine) {
+      if (isMine) {
         if (userRole === 'CUSTOMER_FREE') {
 
           setModalMessage("¿Desea guardar su propia esquela?\n ⚠️¡Recuerde que debe contratar nuestro plan para que su esquela sea enviada!");
@@ -298,9 +262,10 @@ function SelectContacts() {
     setModalVisible(false);
   };
 
-  const handleSubmit = async (is_mine: boolean) => {
+  const handleSubmit = async () => {
+  
     try {
-      if (is_mine) {
+      if (isMine) {
         await createObituary();
       } else {
         moveToNextScreen();
@@ -315,6 +280,63 @@ function SelectContacts() {
     }
   };
 
+  const createObituary = async () => {
+    const contactsWithoutIds = contacts.map(({ id, ...rest }) => rest);
+    const dataToSend = {
+      ...combinedData,
+      contacts: contactsWithoutIds,
+      isMine: isMine,
+    };
+
+    const url = is_newObituary
+      ? BACKEND_API + `/api/obituary/create`
+      : BACKEND_API + `/api/obituary/update/${obituaryId}`;
+
+    const method_type = is_newObituary ? "POST" : "PUT";
+
+    try {
+
+      const authToken = await AsyncStorage.getItem("authToken");
+      const response = await fetch(url, {
+        method: method_type,
+        headers: {
+          "Content-Type": "application/json",
+          Authorization: `Bearer ${authToken}`,
+        },
+        body: JSON.stringify(dataToSend),
+      });
+
+      if (response.ok) {
+        navigation.navigate("obituaries/listMyObituaries" as never);
+      } else {
+        throw new Error("Error en la creación de la esquela");
+      }
+    } catch (error) {
+      const errormssg = is_newObituary
+        ? "Error al crear la esquela.Por favor, inténtelo de nuevo."
+        : "Error al actualizar la esquela.Por favor, inténtelo de nuevo.";
+      window.alert(errormssg);
+    }
+  };
+
+
+  const moveToNextScreen = () => {
+    const contactsWithoutIds = contacts.map(({ id, ...rest }) => rest);
+
+    const dataToSend = {
+        ...combinedData,
+        contacts: contactsWithoutIds,
+    };
+
+    navigation.navigate("obituaries/loadCertificate", { 
+        jsonData: JSON.stringify(dataToSend) ,
+        is_newObituary, 
+        obituaryId,
+        isMine
+    });
+};
+
+
   return isAuthenticated ? (
     <View style={styles.container}>
       <View style={styles.dataContainer}>
@@ -379,24 +401,47 @@ function SelectContacts() {
       <View style={styles.divider} />
       <View style={styles.buttonContainer}>
 
-        <CustomButton
-          title={
-            is_newObituary
-              ? "Cree su propia esquela"
-              : "Actualice su propia esquela"
-          }
-          onPress={() => showConfirmationModal(true)}
+        
+
+
+      {
+        is_newObituary ? (
+          <>
+            <CustomButton
+              title={"Cree su propia esquela"}
+              onPress={() => showConfirmationModal(true)}
+              style={styles.saveButton}
+            />
+
+            <CustomButton
+              title={"Cree y envie su esquela para un ser querido"}
+              onPress={() => showConfirmationModal(false)}
+              style={styles.saveButton}
+            />
+          </>
+        ) : is_mine ? (
+          <CustomButton
+          title="Actualice su esquela"
+          onPress={() => {
+            if (is_mine) {
+              showConfirmationModal(true); 
+            } else {
+              window.alert("Función deshabilitada temporalmente"); 
+            }
+          }}
           style={styles.saveButton}
         />
-        {is_newObituary && (
+        
+        ) : (
           <CustomButton
-            title="Cree y envie su esquela para un ser querido"
-            //onPress={() => showConfirmationModal(false)}
-            onPress={() => alert("Esta función estará disponible en el futuro")}
+            title={"Actualice el certificado de defunción"}
+            onPress={() => showConfirmationModal(false)}
             style={styles.saveButton}
           />
         )
-        }
+      }
+
+
 
       </View>
 
@@ -410,9 +455,9 @@ function SelectContacts() {
           <View style={styles.buttonContainer}>
             <TouchableOpacity
               style={styles.button}
-              onPress={() => handleSubmit(true)}
+              onPress={() => handleSubmit()}
             >
-              <Text style={styles.buttonText}>Aceptar</Text>
+            <Text style={styles.buttonText}>Aceptar</Text>
             </TouchableOpacity>
             <TouchableOpacity
               style={styles.button}


Reviews:
user: login: JavierRodriguezReina
body: The most important functionality works correctly. There are minor bugs that don't affect functionality and need to be improved.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:37:34Z
user: login: hugborang
body: I checked everything, and the functionality seems to be working well. I found grammatical errors in the modal messages, so I will commit the changes. I'll try to fix the Codacy problems in order to have everything correct.

It is important to know that you have to change the .env configuration for the functionality to work
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:57:43Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:42:32Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:42:33Z
user: login: codacy-production[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T11:42:34Z

Review comments:
diff_hunk: @@ -0,0 +1,348 @@
+import { useState } from "react";
+import { View, Text, TextInput, Button, StyleSheet, Image } from "react-native";
+import { useNavigation, NavigationProp, useRoute, RouteProp } from "@react-navigation/native";
+import { Dimensions } from "react-native";
+import { TouchableOpacity } from "react-native";
+import { AUTHORITIES } from "../_util/Authorities";
+import { withAuth } from "../_util/withAuth";
+import * as ImagePicker from "expo-image-picker";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import CustomButton from "@/components/CustomButton";
+import { CustomTextInput } from "@/components/CustomTextInput";
+import { GlobalStyles } from "@/constants/Colors";
+import { ThemedView } from "@/components/ThemedView";
+import CustomModal from "@/components/CustomModal";
+import { BACKEND_API } from "@/constants/Mysc";
+
+type RootStackParamList = {
path: app/certificate/index.tsx
user: login: codacy-production[bot]
body: Codacy has a fix for the issue: [Use an `interface` instead of a `type`.](https://app.codacy.com/gh/ISPP-2425-G9/frontend/pull-requests/83)

```suggestion
interface RootStackParamList {
```
created_at: 2025-03-20T11:42:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -379,24 +401,47 @@
       <View style={styles.divider} />
       <View style={styles.buttonContainer}>
 
-        <CustomButton
-          title={
-            is_newObituary
-              ? "Cree su propia esquela"
-              : "Actualice su propia esquela"
-          }
-          onPress={() => showConfirmationModal(true)}
+        
+
+
+      {
+        is_newObituary ? (
+          <>
+            <CustomButton
+              title={"Cree su propia esquela"}
+              onPress={() => showConfirmationModal(true)}
+              style={styles.saveButton}
+            />
+
+            <CustomButton
+              title={"Cree y envie su esquela para un ser querido"}
+              onPress={() => showConfirmationModal(false)}
+              style={styles.saveButton}
+            />
+          </>
+        ) : is_mine ? (
+          <CustomButton
+          title="Actualice su esquela"
+          onPress={() => {
+            if (is_mine) {
+              showConfirmationModal(true); 
path: app/obituaries/selectContacts.tsx
user: login: codacy-production[bot]
body: Codacy has a fix for the issue: [Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.](https://app.codacy.com/gh/ISPP-2425-G9/frontend/pull-requests/83)

```suggestion
              void showConfirmationModal(true); 
```
created_at: 2025-03-20T11:42:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -257,12 +267,14 @@
   };
 
   const handleSubmit = () => {
+    console.log(is_mine, "is_mine");
     if (validateForm()) {
       const jsonData = JSON.stringify(formData, null, 2);
       navigation.navigate("obituaries/selectContacts" as never, {
         jsonData: jsonData,
         is_newObituary,
         obituaryId,
+        is_mine: is_mine !== undefined ? is_mine : undefined, 
path: app/obituaries/createObituary.tsx
user: login: codacy-production[bot]
body: Codacy has a fix for the issue: [Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read.](https://app.codacy.com/gh/ISPP-2425-G9/frontend/pull-requests/83)

```suggestion
        is_mine: is_mine ?? undefined, 
```
created_at: 2025-03-20T11:42:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: hugborang
created_at: 2025-03-20T11:14:59Z
body: We should add a successful message when uploading a certificate 🟢 
author_association: CONTRIBUTOR
user: login: IsaacSolisPadilla
created_at: 2025-03-20T14:43:12Z
body: I have tested all the new features created in this branch and everything worked fine. I tested everything thoroughly, including edge cases and scenarios designed to make the application crash or fail.
author_association: NONE