diff --git a/apps/embed-iframe/src/ClientsPermissions.ts b/apps/embed-iframe/src/ClientsPermissions.ts index 418d712aab..d24c7fcb19 100644 --- a/apps/embed-iframe/src/ClientsPermissions.ts +++ b/apps/embed-iframe/src/ClientsPermissions.ts @@ -2,6 +2,7 @@ interface Permissions { origins: string[]; login: boolean; operations: boolean; + signPayload: boolean; } const clientPermissions: Record = { @@ -12,6 +13,7 @@ const clientPermissions: Record = { ], login: true, operations: false, + signPayload: false, }, }; diff --git a/apps/embed-iframe/src/EmbeddedComponent.tsx b/apps/embed-iframe/src/EmbeddedComponent.tsx index d3f672a061..54e0e903d7 100644 --- a/apps/embed-iframe/src/EmbeddedComponent.tsx +++ b/apps/embed-iframe/src/EmbeddedComponent.tsx @@ -13,6 +13,7 @@ import { useOperationModal } from "./operationModalHooks"; import { sendResponse } from "./utils"; import "./EmbeddedComponent.scss"; import { useEmbedApp } from "./EmbedAppContext"; +import { useSignPayloadModal } from "./signPayloadModalHooks"; export function EmbeddedComponent() { const { getNetwork, getUserData, setNetwork, setUserData, setLoginOptions, setDAppOrigin } = @@ -21,6 +22,8 @@ export function EmbeddedComponent() { const { onOpen: openLoginModal, modalElement: loginModalElement } = useLoginModal(); const { onOpen: openOperationModal, modalElement: operationModalElement } = useOperationModal(); + const { onOpen: openSignPayloadModal, modalElement: signPayloadModalElement } = + useSignPayloadModal(); useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -86,6 +89,10 @@ export function EmbeddedComponent() { openOperationModal(data.operations); } break; + case "sign_request": + if (validateUserSession(data.type)) { + openSignPayloadModal(data.signingType, data.payload); + } } } catch { /* empty */ @@ -161,6 +168,15 @@ export function EmbeddedComponent() { return false; } break; + case "sign_request": + if (!clientPermissions.signPayload) { + sendResponse({ + type: toMatchingResponseType(request.type), + error: "no_permissions", + errorMessage: "No permissions found for sign actions", + }); + return false; + } } return true; }; @@ -188,6 +204,7 @@ export function EmbeddedComponent() { {loginModalElement} {operationModalElement} + {signPayloadModalElement} ); } diff --git a/apps/embed-iframe/src/LoginModalContext.tsx b/apps/embed-iframe/src/LoginModalContext.tsx index 6496d35c4d..d10a06389d 100644 --- a/apps/embed-iframe/src/LoginModalContext.tsx +++ b/apps/embed-iframe/src/LoginModalContext.tsx @@ -25,7 +25,7 @@ export const LoginModalProvider = ({ children }: PropsWithChildren) => { export const useLoginModalContext = (): LoginModalContextState => { const context = useContext(LoginModalContext); if (context === undefined) { - throw new Error("useLoginModal must be used within a LoginModalProvider"); + throw new Error("useLoginModalContext must be used within a LoginModalProvider"); } return context; }; diff --git a/apps/embed-iframe/src/OperationModalContext.tsx b/apps/embed-iframe/src/OperationModalContext.tsx index 093d8ba483..a28c3fe60d 100644 --- a/apps/embed-iframe/src/OperationModalContext.tsx +++ b/apps/embed-iframe/src/OperationModalContext.tsx @@ -41,7 +41,7 @@ export const OperationModalProvider = ({ children }: PropsWithChildren) => { export const useOperationModalContext = (): OperationModalContextState => { const context = useContext(OperationModalContext); if (context === undefined) { - throw new Error("useOperationModal must be used within a OperationModalProvider"); + throw new Error("useOperationModalContext must be used within a OperationModalProvider"); } return context; }; diff --git a/apps/embed-iframe/src/SignPayloadModalContent.tsx b/apps/embed-iframe/src/SignPayloadModalContent.tsx new file mode 100644 index 0000000000..80de3df22c --- /dev/null +++ b/apps/embed-iframe/src/SignPayloadModalContent.tsx @@ -0,0 +1,92 @@ +import { Box, Flex, Heading, Text, VStack } from "@chakra-ui/react"; +import * as Auth from "@umami/social-auth"; + +import { UmamiLogoIcon } from "./assets/icons/UmamiLogo"; + +import { getErrorContext } from "./imported/utils/getErrorContext"; +import { withTimeout } from "./imported/utils/withTimeout"; +import { sendOperationErrorResponse, sendResponse, toTezosNetwork } from "./utils"; +import { makeToolkit } from "@umami/tezos"; +import { useEmbedApp } from "./EmbedAppContext"; +import { useColor } from "./imported/style/useColor"; +import { LoginButtonComponent } from "./LoginButtonComponent"; +import { getDAppByOrigin } from "./ClientsPermissions"; +import { useSignPayloadModalContext } from "./SignPayloadModalContext"; +import { decodePayload } from "@umami/core"; + +const SIGN_TIMEOUT = 5 * 60 * 1000; // 5 minutes + +export const SignPayloadModalContent = () => { + const { onClose, setIsLoading, signingType, payload } = useSignPayloadModalContext(); + const { getNetwork, getUserData, getDAppOrigin } = useEmbedApp(); + + const color = useColor(); + const dAppName = getDAppByOrigin(getDAppOrigin()); + + const onClick = async () => { + setIsLoading(true); + try { + const { secretKey } = await withTimeout( + async () => Auth.forIDP(getUserData()!.typeOfLogin).getCredentials(), + SIGN_TIMEOUT + ); + const toolkit = await makeToolkit({ + type: "social", + secretKey, + network: toTezosNetwork(getNetwork()!), + }); + + const result = await toolkit.signer.sign(payload!); + + sendResponse({ + type: "sign_response", + signingType: signingType!, + signature: result.prefixSig, + }); + } catch (error) { + sendOperationErrorResponse(getErrorContext(error).description); + } finally { + setIsLoading(false); + onClose(); + } + }; + + return ( + + + + + + + Sign Payload + + + + + {dAppName ? dAppName : getDAppOrigin()} + + is requesting permission to sign this payload + + + + + + {decodePayload(payload!)} + + + + + ); +}; diff --git a/apps/embed-iframe/src/SignPayloadModalContext.tsx b/apps/embed-iframe/src/SignPayloadModalContext.tsx new file mode 100644 index 0000000000..1f3a404c5b --- /dev/null +++ b/apps/embed-iframe/src/SignPayloadModalContext.tsx @@ -0,0 +1,50 @@ +import { useDisclosure } from "@chakra-ui/react"; +import { createContext, PropsWithChildren, useContext, useState } from "react"; +import { SigningType } from "@airgap/beacon-types"; + +interface SignPayloadModalContextState { + isOpen: boolean; + onOpen: () => void; + onClose: () => void; + isLoading: boolean; + setIsLoading: (isLoading: boolean) => void; + signingType: SigningType | null; + setSigningType: (signingType: SigningType) => void; + payload: string | null; + setPayload: (payload: string) => void; +} + +const SignPayloadModalContext = createContext(undefined); + +export const SignPayloadModalProvider = ({ children }: PropsWithChildren) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const [isLoading, setIsLoading] = useState(false); + const [signingType, setSigningType] = useState(null); + const [payload, setPayload] = useState(null); + + return ( + + {children} + + ); +}; + +export const useSignPayloadModalContext = (): SignPayloadModalContextState => { + const context = useContext(SignPayloadModalContext); + if (context === undefined) { + throw new Error("useSignPayloadModalContext must be used within a SignPayloadModalProvider"); + } + return context; +}; diff --git a/apps/embed-iframe/src/main.tsx b/apps/embed-iframe/src/main.tsx index da69dcc2d9..88c4146525 100644 --- a/apps/embed-iframe/src/main.tsx +++ b/apps/embed-iframe/src/main.tsx @@ -10,6 +10,7 @@ import { LoginModalProvider } from "./LoginModalContext"; import { OperationModalProvider } from "./OperationModalContext"; import { Analytics } from "@vercel/analytics/react"; +import { SignPayloadModalProvider } from "./SignPayloadModalContext"; const rootElement = document.getElementById("root"); ReactDOM.createRoot(rootElement!).render( @@ -18,9 +19,11 @@ ReactDOM.createRoot(rootElement!).render( - - - + + + + + diff --git a/apps/embed-iframe/src/signPayloadModalHooks.tsx b/apps/embed-iframe/src/signPayloadModalHooks.tsx new file mode 100644 index 0000000000..ed4c3f5539 --- /dev/null +++ b/apps/embed-iframe/src/signPayloadModalHooks.tsx @@ -0,0 +1,42 @@ +import { Center, Modal, ModalCloseButton, ModalContent } from "@chakra-ui/react"; + +import { sendSignPayloadErrorResponse } from "./utils"; +import { useSignPayloadModalContext } from "./SignPayloadModalContext"; +import { ModalLoadingOverlay } from "./ModalLoadingOverlay"; +import { SignPayloadModalContent } from "./SignPayloadModalContent"; +import { SigningType } from "@airgap/beacon-types"; + +export const useSignPayloadModal = () => { + const { isOpen, onOpen, onClose, isLoading, setSigningType, setPayload } = + useSignPayloadModalContext(); + + const onModalCLose = () => { + sendSignPayloadErrorResponse("User closed the modal"); + onClose(); + }; + + return { + modalElement: ( +
+ + + + + {isLoading && } + + +
+ ), + onOpen: (signingType: SigningType, payload: string) => { + setSigningType(signingType); + setPayload(payload); + onOpen(); + }, + }; +}; diff --git a/apps/embed-iframe/src/utils.ts b/apps/embed-iframe/src/utils.ts index fb4f473b99..ae31954aec 100644 --- a/apps/embed-iframe/src/utils.ts +++ b/apps/embed-iframe/src/utils.ts @@ -21,6 +21,14 @@ export const sendOperationErrorResponse = (errorMessage: string) => { }); }; +export const sendSignPayloadErrorResponse = (errorMessage: string) => { + sendResponse({ + type: "sign_response", + error: "sign_failed", + errorMessage, + }); +}; + export const toTezosNetwork = (network: Network) => { switch (network) { case "ghostnet":