import React, { useCallback, useContext, useEffect, useMemo, useState, } from 'react'; import { Box, Button, ButtonBase, Dialog, DialogActions, DialogContent, Input, MenuItem, Select, Tab, Tabs, useTheme, } from '@mui/material'; import { useDropzone } from 'react-dropzone'; import { useHandlePrivateApps } from '../../hooks/useHandlePrivateApps'; import { groupsPropertiesAtom, memberGroupsAtom, myGroupsWhereIAmAdminAtom, } from '../../atoms/global'; import { Label } from '../Group/AddGroup'; import { Spacer } from '../../common/Spacer'; import { AppCircle, AppCircleContainer, AppCircleLabel, PublishQAppChoseFile, PublishQAppInfo, } from './Apps-styles'; import AddIcon from '@mui/icons-material/Add'; import ImageUploader from '../../common/ImageUploader'; import { QORTAL_APP_CONTEXT } from '../../App'; import { fileToBase64 } from '../../utils/fileReading'; import { objectToBase64 } from '../../qdn/encryption/group-encryption'; import { getFee } from '../../background/background.ts'; import { useAtom } from 'jotai'; import { useTranslation } from 'react-i18next'; import { useSortedMyNames } from '../../hooks/useSortedMyNames'; const maxFileSize = 50 * 1024 * 1024; // 50MB export const AppsPrivate = ({ myName, myAddress }) => { const [names, setNames] = useState([]); const [name, setName] = useState(0); const { openApp } = useHandlePrivateApps(); const [file, setFile] = useState(null); const [logo, setLogo] = useState(null); const [qortalUrl, setQortalUrl] = useState(''); const [selectedGroup, setSelectedGroup] = useState(0); const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0); const [groupsProperties] = useAtom(groupsPropertiesAtom); const [myGroupsWhereIAmAdminFromGlobal] = useAtom(myGroupsWhereIAmAdminAtom); const myGroupsWhereIAmAdmin = useMemo(() => { return myGroupsWhereIAmAdminFromGlobal?.filter( (group) => groupsProperties[group?.groupId]?.isOpen === false ); }, [myGroupsWhereIAmAdminFromGlobal, groupsProperties]); const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false); const { show, setInfoSnackCustom, setOpenSnackGlobal } = useContext(QORTAL_APP_CONTEXT); const [memberGroups] = useAtom(memberGroupsAtom); const theme = useTheme(); const { t } = useTranslation(['auth', 'core', 'group']); const myGroupsPrivate = useMemo(() => { return memberGroups?.filter( (group) => groupsProperties[group?.groupId]?.isOpen === false ); }, [memberGroups, groupsProperties]); const [privateAppValues, setPrivateAppValues] = useState({ name: '', service: 'DOCUMENT', identifier: '', groupId: 0, }); const [newPrivateAppValues, setNewPrivateAppValues] = useState({ service: 'DOCUMENT', identifier: '', name: '', }); const mySortedNames = useSortedMyNames(names, myName); const { getRootProps, getInputProps } = useDropzone({ accept: { 'application/zip': ['.zip'], // Only accept zip files }, maxSize: maxFileSize, multiple: false, // Disable multiple file uploads onDrop: (acceptedFiles) => { if (acceptedFiles.length > 0) { setFile(acceptedFiles[0]); // Set the file name } }, onDropRejected: (fileRejections) => { fileRejections.forEach(({ file, errors }) => { errors.forEach((error) => { if (error.code === 'file-too-large') { console.error( t('core:message.error.file_too_large', { filename: file.name, size: maxFileSize / (1024 * 1024), postProcess: 'capitalizeFirstChar', }) ); } }); }); }, }); const addPrivateApp = async () => { try { if (privateAppValues?.groupId === 0) return; await openApp(privateAppValues, true); } catch (error) { console.error(error); } }; const clearFields = () => { setPrivateAppValues({ name: '', service: 'DOCUMENT', identifier: '', groupId: 0, }); setNewPrivateAppValues({ service: 'DOCUMENT', identifier: '', name: '', }); setFile(null); setValueTabPrivateApp(0); setSelectedGroup(0); setLogo(null); }; const publishPrivateApp = async () => { try { if (selectedGroup === 0) return; if (!logo) throw new Error( t('core:message.generic.select_image', { postProcess: 'capitalizeFirstChar', }) ); if (!myName) throw new Error( t('core:message.generic.name_publish', { postProcess: 'capitalizeFirstChar', }) ); if (!newPrivateAppValues?.name) throw new Error( t('core:message.error.app_need_name', { postProcess: 'capitalizeFirstChar', }) ); const base64Logo = await fileToBase64(logo); const base64App = await fileToBase64(file); const objectToSave = { app: base64App, logo: base64Logo, name: newPrivateAppValues.name, }; const object64 = await objectToBase64(objectToSave); const decryptedData = await window.sendMessage( 'ENCRYPT_QORTAL_GROUP_DATA', { base64: object64, groupId: selectedGroup, } ); if (decryptedData?.error) { throw new Error( decryptedData?.error || t('core:message.error.encrypt_app', { postProcess: 'capitalizeFirstChar', }) ); } const fee = await getFee('ARBITRARY'); await show({ message: t('core:message.question.publish_app', { postProcess: 'capitalizeFirstChar', }), publishFee: fee.fee + ' QORT', }); await new Promise((res, rej) => { window .sendMessage('publishOnQDN', { data: decryptedData, identifier: newPrivateAppValues?.identifier, service: newPrivateAppValues?.service, uploadType: 'base64', name, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirstChar', }) ); }); }); openApp( { identifier: newPrivateAppValues?.identifier, service: newPrivateAppValues?.service, name, groupId: selectedGroup, }, true ); clearFields(); } catch (error) { setOpenSnackGlobal(true); setInfoSnackCustom({ type: 'error', message: error?.message || t('core:message.error.publish_app', { postProcess: 'capitalizeFirstChar', }), }); } }; const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValueTabPrivateApp(newValue); }; function a11yProps(index: number) { return { id: `simple-tab-${index}`, 'aria-controls': `simple-tabpanel-${index}`, }; } const getNames = useCallback(async () => { if (!myAddress) return; try { const res = await fetch( `${getBaseApiReact()}/names/address/${myAddress}?limit=0` ); const data = await res.json(); setNames(data?.map((item) => item.name)); } catch (error) { console.error(error); } }, [myAddress]); useEffect(() => { getNames(); }, [getNames]); return ( <> { setIsOpenPrivateModal(true); }} sx={{ width: '80px', }} > Private {isOpenPrivateModal && ( { if (e.key === 'Enter') { if (valueTabPrivateApp === 0) { if ( !privateAppValues.name || !privateAppValues.service || !privateAppValues.identifier || !privateAppValues?.groupId ) return; addPrivateApp(); } } }} maxWidth="md" fullWidth={true} PaperProps={{ style: { backgroundColor: theme.palette.background.paper, boxShadow: 'none', }, }} > {valueTabPrivateApp === 0 && ( <> setPrivateAppValues((prev) => { return { ...prev, name: e.target.value, }; }) } /> setPrivateAppValues((prev) => { return { ...prev, identifier: e.target.value, }; }) } /> )} {valueTabPrivateApp === 1 && ( <> {t('core:message.generic.select_zip', { postProcess: 'capitalizeFirstChar', })} {` 50mb MB max`} {file && ( <> {`Selected: (${file?.name})`} )} {' '} {file ? t('core:action.change_file', { postProcess: 'capitalizeFirstChar', }) : t('core:action.choose_file', { postProcess: 'capitalizeFirstChar', })} setNewPrivateAppValues((prev) => { return { ...prev, identifier: e.target.value, }; }) } /> setNewPrivateAppValues((prev) => { return { ...prev, name: e.target.value, }; }) } /> setLogo(file)}> {logo?.name} )} )} ); };