diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx
index cef2d8d..902c6d8 100644
--- a/src/components/Apps/AppViewer.tsx
+++ b/src/components/Apps/AppViewer.tsx
@@ -46,7 +46,7 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef)
if(isDevMode){
resetHistory()
- if(!app?.isPreview){
+ if(!app?.isPreview || app?.isPrivate){
setUrl(app?.url + `?time=${Date.now()}`)
}
return
diff --git a/src/components/Apps/AppsDesktop.tsx b/src/components/Apps/AppsDesktop.tsx
index b29f9d5..20a3d54 100644
--- a/src/components/Apps/AppsDesktop.tsx
+++ b/src/components/Apps/AppsDesktop.tsx
@@ -450,7 +450,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
}}>
-
+
)}
@@ -479,6 +479,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
isSelected={tab?.tabId === selectedTab?.tabId}
app={tab}
ref={iframeRefs.current[tab.tabId]}
+ isDevMode={tab?.service ? false : true}
/>
);
})}
@@ -494,7 +495,7 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
}}>
-
+
>
)}
diff --git a/src/components/Apps/AppsHomeDesktop.tsx b/src/components/Apps/AppsHomeDesktop.tsx
index 7c548d1..32f5abe 100644
--- a/src/components/Apps/AppsHomeDesktop.tsx
+++ b/src/components/Apps/AppsHomeDesktop.tsx
@@ -16,11 +16,13 @@ import { Spacer } from "../../common/Spacer";
import { SortablePinnedApps } from "./SortablePinnedApps";
import { extractComponents } from "../Chat/MessageDisplay";
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
+import { AppsPrivate } from "./AppsPrivate";
export const AppsHomeDesktop = ({
setMode,
myApp,
myWebsite,
availableQapps,
+ myName
}) => {
const [qortalUrl, setQortalUrl] = useState('')
@@ -135,7 +137,7 @@ export const AppsHomeDesktop = ({
Library
-
+
{
- const isSelectedAppPinned = !!sortablePinnedApps?.find(
- (item) =>
- item?.name === selectedTab?.name && item?.service === selectedTab?.service
- );
+ const isSelectedAppPinned = useMemo(()=> {
+ if(selectedTab?.isPrivate){
+ return !!sortablePinnedApps?.find(
+ (item) =>
+ item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name && item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service && item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier
+ );
+ } else {
+ return !!sortablePinnedApps?.find(
+ (item) =>
+ item?.name === selectedTab?.name && item?.service === selectedTab?.service
+ );
+ }
+ }, [selectedTab,sortablePinnedApps])
return (
{
if (isSelectedAppPinned) {
// Remove the selected app if it is pinned
- updatedApps = prev.filter(
- (item) =>
- !(
- item?.name === selectedTab?.name &&
- item?.service === selectedTab?.service
- )
- );
+ if(selectedTab?.isPrivate){
+ updatedApps = prev.filter(
+ (item) =>
+ !(
+ item?.privateAppProperties?.name === selectedTab?.privateAppProperties?.name &&
+ item?.privateAppProperties?.service === selectedTab?.privateAppProperties?.service &&
+ item?.privateAppProperties?.identifier === selectedTab?.privateAppProperties?.identifier
+ )
+ );
+ } else {
+ updatedApps = prev.filter(
+ (item) =>
+ !(
+ item?.name === selectedTab?.name &&
+ item?.service === selectedTab?.service
+ )
+ );
+ }
+
} else {
// Add the selected app if it is not pinned
- updatedApps = [
+ if(selectedTab?.isPrivate){
+ updatedApps = [
...prev,
{
- name: selectedTab?.name,
- service: selectedTab?.service,
+ isPreview: true,
+ isPrivate: true,
+ privateAppProperties: {
+ ...(selectedTab?.privateAppProperties || {})
+ }
+
},
];
+ } else {
+ updatedApps = [
+ ...prev,
+ {
+ name: selectedTab?.name,
+ service: selectedTab?.service,
+ },
+ ];
+ }
+
}
saveToLocalStorage(
@@ -338,9 +374,15 @@ export const AppsNavBarDesktop = ({disableBack}) => {
-
+
+ )}
);
diff --git a/src/components/Apps/AppsPrivate.tsx b/src/components/Apps/AppsPrivate.tsx
new file mode 100644
index 0000000..b014cd2
--- /dev/null
+++ b/src/components/Apps/AppsPrivate.tsx
@@ -0,0 +1,542 @@
+import React, { useContext, useState } from "react";
+import {
+ Avatar,
+ Box,
+ Button,
+ ButtonBase,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Input,
+ MenuItem,
+ Select,
+ Tab,
+ Tabs,
+ Typography,
+} from "@mui/material";
+import { useDropzone } from "react-dropzone";
+import { useHandlePrivateApps } from "./useHandlePrivateApps";
+import { useRecoilState, useSetRecoilState } from "recoil";
+import { myGroupsWhereIAmAdminAtom } from "../../atoms/global";
+import { Label } from "../Group/AddGroup";
+import { Spacer } from "../../common/Spacer";
+import {
+ Add,
+ AppCircle,
+ AppCircleContainer,
+ AppCircleLabel,
+ PublishQAppChoseFile,
+ PublishQAppInfo,
+} from "./Apps-styles";
+import ImageUploader from "../../common/ImageUploader";
+import { isMobile, MyContext } from "../../App";
+import { fileToBase64 } from "../../utils/fileReading";
+import { objectToBase64 } from "../../qdn/encryption/group-encryption";
+import { getFee } from "../../background";
+
+const maxFileSize = 50 * 1024 * 1024; // 50MB
+
+export const AppsPrivate = ({myName}) => {
+ 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 [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState(
+ myGroupsWhereIAmAdminAtom
+ );
+ const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false);
+ const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } = useContext(MyContext);
+
+ const [privateAppValues, setPrivateAppValues] = useState({
+ name: "",
+ service: "DOCUMENT",
+ identifier: "",
+ groupId: 0,
+ });
+
+ const [newPrivateAppValues, setNewPrivateAppValues] = useState({
+ service: "DOCUMENT",
+ identifier: "",
+ name: "",
+ });
+ 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(
+ `File ${file.name} is too large. Max size allowed is ${
+ maxFileSize / (1024 * 1024)
+ } MB.`
+ );
+ }
+ });
+ });
+ },
+ });
+
+ const addPrivateApp = async () => {
+ try {
+ if (privateAppValues?.groupId === 0) return;
+
+ await openApp(privateAppValues, true);
+ } catch (error) {
+ console.log('error', error?.message)
+
+ }
+ };
+
+ 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("Please select an image for a logo");
+ if (!myName) throw new Error("You need a Qortal name to publish");
+ if (!newPrivateAppValues?.name) throw new Error("Your app needs a name");
+ 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 || "Unable to encrypt app. App not published"
+ );
+ }
+ const fee = await getFee("ARBITRARY");
+
+ await show({
+ message: "Would you like to publish this app?",
+ publishFee: fee.fee + " QORT",
+ });
+ await new Promise((res, rej) => {
+ window
+ .sendMessage("publishOnQDN", {
+ data: decryptedData,
+ identifier: newPrivateAppValues?.identifier,
+ service: newPrivateAppValues?.service,
+ })
+ .then((response) => {
+ if (!response?.error) {
+ res(response);
+ return;
+ }
+ rej(response.error);
+ })
+ .catch((error) => {
+ rej(error.message || "An error occurred");
+ });
+ });
+ openApp(
+ {
+ identifier: newPrivateAppValues?.identifier,
+ service: newPrivateAppValues?.service,
+ name: myName,
+ groupId: selectedGroup,
+ },
+ true
+ );
+ clearFields();
+ } catch (error) {
+ setOpenSnackGlobal(true)
+ setInfoSnackCustom({
+ type: "error",
+ message: error?.message || "Unable to publish app",
+ });
+ }
+ };
+
+ const handleChange = (event: React.SyntheticEvent, newValue: number) => {
+ setValueTabPrivateApp(newValue);
+ };
+
+ function a11yProps(index: number) {
+ return {
+ id: `simple-tab-${index}`,
+ "aria-controls": `simple-tabpanel-${index}`,
+ };
+ }
+ return (
+ <>
+ {
+ setIsOpenPrivateModal(true);
+ }}
+ sx={{
+ width: "80px",
+ }}
+ >
+
+
+ +
+
+ Private
+
+
+ {isOpenPrivateModal && (
+
+ )}
+ >
+ );
+};
diff --git a/src/components/Apps/SortablePinnedApps.tsx b/src/components/Apps/SortablePinnedApps.tsx
index 15f54b9..98c2287 100644
--- a/src/components/Apps/SortablePinnedApps.tsx
+++ b/src/components/Apps/SortablePinnedApps.tsx
@@ -1,18 +1,21 @@
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { DndContext, closestCenter } from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable';
import { KeyboardSensor, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
import { CSS } from '@dnd-kit/utilities';
import { Avatar, ButtonBase } from '@mui/material';
import { AppCircle, AppCircleContainer, AppCircleLabel } from './Apps-styles';
-import { getBaseApiReact } from '../../App';
+import { getBaseApiReact, MyContext } from '../../App';
import { executeEvent } from '../../utils/events';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from '../../atoms/global';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { saveToLocalStorage } from './AppsNavBar';
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
-
+import LockIcon from "@mui/icons-material/Lock";
+import { useHandlePrivateApps } from './useHandlePrivateApps';
const SortableItem = ({ id, name, app, isDesktop }) => {
+ const {openApp} = useHandlePrivateApps()
+
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
@@ -28,17 +31,27 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
return (
- {
- executeEvent("addTab", {
- data: app
- })
+ onClick={async ()=> {
+ if(app?.isPrivate){
+ try {
+ await openApp(app?.privateAppProperties)
+ } catch (error) {
+ console.error(error)
+ }
+
+ } else {
+ executeEvent("addTab", {
+ data: app
+ })
+ }
+
}}
>
{
border: "none",
}}
>
-
+ ) : (
+ {
}
}}
alt={app?.metadata?.title || app?.name}
- src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
+ src={ app?.privateAppProperties?.logo ? app?.privateAppProperties?.logo :`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
@@ -72,10 +93,19 @@ const SortableItem = ({ id, name, app, isDesktop }) => {
alt="center-icon"
/>
+ )}
+
-
+ {app?.isPrivate ? (
+
+ {`${app?.privateAppProperties?.appName || "Private"}`}
+
+ ) : (
+
{app?.metadata?.title || app?.name}
+ )}
+
diff --git a/src/components/Apps/TabComponent.tsx b/src/components/Apps/TabComponent.tsx
index aca6b55..ecf17a7 100644
--- a/src/components/Apps/TabComponent.tsx
+++ b/src/components/Apps/TabComponent.tsx
@@ -5,6 +5,7 @@ import { getBaseApiReact } from '../../App';
import { Avatar, ButtonBase } from '@mui/material';
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from '../../utils/events';
+import LockIcon from "@mui/icons-material/Lock";
const TabComponent = ({isSelected, app}) => {
return (
@@ -34,25 +35,34 @@ const TabComponent = ({isSelected, app}) => {
} src={NavCloseTab}/>
) }
-
-
-
+ {app?.isPrivate && !app?.privateAppProperties?.logo ? (
+
+ ) : (
+
+
+
+ )}
)
diff --git a/src/components/Apps/useHandlePrivateApps.tsx b/src/components/Apps/useHandlePrivateApps.tsx
new file mode 100644
index 0000000..2eaa5f9
--- /dev/null
+++ b/src/components/Apps/useHandlePrivateApps.tsx
@@ -0,0 +1,237 @@
+import React, { useContext, useState } from "react";
+import { executeEvent } from "../../utils/events";
+import { getBaseApiReact, MyContext } from "../../App";
+import { createEndpoint } from "../../background";
+import { useRecoilState, useSetRecoilState } from "recoil";
+import {
+ settingsLocalLastUpdatedAtom,
+ sortablePinnedAppsAtom,
+} from "../../atoms/global";
+import { saveToLocalStorage } from "./AppsNavBarDesktop";
+import { base64ToBlobUrl } from "../../utils/fileReading";
+import { base64ToUint8Array } from "../../qdn/encryption/group-encryption";
+import { uint8ArrayToObject } from "../../backgroundFunctions/encryption";
+
+export const useHandlePrivateApps = () => {
+ const [status, setStatus] = useState("");
+ const {
+ openSnackGlobal,
+ setOpenSnackGlobal,
+ infoSnackCustom,
+ setInfoSnackCustom,
+ } = useContext(MyContext);
+ const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
+ sortablePinnedAppsAtom
+ );
+ const setSettingsLocalLastUpdated = useSetRecoilState(
+ settingsLocalLastUpdatedAtom
+ );
+ const openApp = async (
+ privateAppProperties,
+ addToPinnedApps,
+ setLoadingStatePrivateApp
+ ) => {
+ try {
+
+
+ if(setLoadingStatePrivateApp){
+ setLoadingStatePrivateApp(`Downloading and decrypting private app.`);
+
+ }
+ setOpenSnackGlobal(true);
+
+ setInfoSnackCustom({
+ type: "info",
+ message: "Fetching app data",
+ duration: null
+ });
+ const urlData = `${getBaseApiReact()}/arbitrary/${
+ privateAppProperties?.service
+ }/${privateAppProperties?.name}/${
+ privateAppProperties?.identifier
+ }?encoding=base64`;
+ let data;
+ try {
+ const responseData = await fetch(urlData, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if(!responseData?.ok){
+ if(setLoadingStatePrivateApp){
+ setLoadingStatePrivateApp("Error! Unable to download private app.");
+ }
+
+ throw new Error("Unable to fetch app");
+ }
+
+ data = await responseData.text();
+ if (data?.error) {
+ if(setLoadingStatePrivateApp){
+
+ setLoadingStatePrivateApp("Error! Unable to download private app.");
+ }
+ throw new Error("Unable to fetch app");
+ }
+ } catch (error) {
+ if(setLoadingStatePrivateApp){
+
+ setLoadingStatePrivateApp("Error! Unable to download private app.");
+ }
+ throw error;
+ }
+
+ let decryptedData;
+ // eslint-disable-next-line no-useless-catch
+ try {
+ decryptedData = await window.sendMessage(
+ "DECRYPT_QORTAL_GROUP_DATA",
+
+ {
+ base64: data,
+ groupId: privateAppProperties?.groupId,
+ }
+ );
+ if (decryptedData?.error) {
+ if(setLoadingStatePrivateApp){
+
+ setLoadingStatePrivateApp("Error! Unable to decrypt private app.");
+ }
+ throw new Error(decryptedData?.error);
+ }
+ } catch (error) {
+ if(setLoadingStatePrivateApp){
+
+ setLoadingStatePrivateApp("Error! Unable to decrypt private app.");
+ }
+ throw error;
+ }
+
+ try {
+ const convertToUint = base64ToUint8Array(decryptedData);
+ const UintToObject = uint8ArrayToObject(convertToUint);
+
+ if (decryptedData) {
+ setInfoSnackCustom({
+ type: "info",
+ message: "Building app",
+ });
+ const endpoint = await createEndpoint(
+ `/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true`
+ );
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "text/plain",
+ },
+ body: UintToObject?.app,
+ });
+ const previewPath = await response.text();
+ const refreshfunc = async (tabId, privateAppProperties) => {
+ const checkIfPreviewLinkStillWorksUrl = await createEndpoint(
+ `/render/hash/HmtnZpcRPwisMfprUXuBp27N2xtv5cDiQjqGZo8tbZS?secret=E39WTiG4qBq3MFcMPeRZabtQuzyfHg9ZuR5SgY7nW1YH`
+ );
+ const res = await fetch(checkIfPreviewLinkStillWorksUrl);
+ if (res.ok) {
+ executeEvent("refreshApp", {
+ tabId: tabId,
+ });
+ } else {
+ const endpoint = await createEndpoint(
+ `/arbitrary/APP/${privateAppProperties?.name}/zip?preview=true`
+ );
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "text/plain",
+ },
+ body: UintToObject?.app,
+ });
+ const previewPath = await response.text();
+ executeEvent("updateAppUrl", {
+ tabId: tabId,
+ url: await createEndpoint(previewPath),
+ });
+
+ setTimeout(() => {
+ executeEvent("refreshApp", {
+ tabId: tabId,
+ });
+ }, 300);
+ }
+ };
+
+ const appName = UintToObject?.name;
+ const logo = UintToObject?.logo
+ ? `data:image/png;base64,${UintToObject?.logo}`
+ : null;
+
+ const dataBody = {
+ url: await createEndpoint(previewPath),
+ isPreview: true,
+ isPrivate: true,
+ privateAppProperties: { ...privateAppProperties, logo, appName },
+ filePath: "",
+ refreshFunc: (tabId) => {
+ refreshfunc(tabId, privateAppProperties);
+ },
+ };
+ executeEvent("addTab", {
+ data: dataBody,
+ });
+ setInfoSnackCustom({
+ type: "success",
+ message: "Opened",
+ });
+ if(setLoadingStatePrivateApp){
+
+ setLoadingStatePrivateApp(``);
+ }
+ if (addToPinnedApps) {
+ setSortablePinnedApps((prev) => {
+ const updatedApps = [
+ ...prev,
+ {
+ isPrivate: true,
+ isPreview: true,
+ privateAppProperties: {
+ ...privateAppProperties,
+ logo,
+ appName,
+ },
+ },
+ ];
+
+ saveToLocalStorage(
+ "ext_saved_settings",
+ "sortablePinnedApps",
+ updatedApps
+ );
+ return updatedApps;
+ });
+ setSettingsLocalLastUpdated(Date.now());
+ }
+ }
+ } catch (error) {
+ if(setLoadingStatePrivateApp){
+
+ setLoadingStatePrivateApp(`Error! ${error?.message || 'Unable to build private app.'}`);
+ }
+ throw error
+ }
+ }
+ catch (error) {
+ setInfoSnackCustom({
+ type: "error",
+ message: error?.message || "Unable to fetch app",
+ });
+ }
+
+ };
+ return {
+ openApp,
+ status,
+ };
+};
diff --git a/src/components/ContextMenuPinnedApps.tsx b/src/components/ContextMenuPinnedApps.tsx
index be0ae46..bb64a4c 100644
--- a/src/components/ContextMenuPinnedApps.tsx
+++ b/src/components/ContextMenuPinnedApps.tsx
@@ -124,11 +124,19 @@ export const ContextMenuPinnedApps = ({ children, app, isMine }) => {