Skip to content

Commit

Permalink
[embed] Add SignPayloadModal to embed iframe
Browse files Browse the repository at this point in the history
  • Loading branch information
asiia-trilitech committed Sep 4, 2024
1 parent 9ba5aad commit f3532d7
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 5 deletions.
2 changes: 2 additions & 0 deletions apps/embed-iframe/src/ClientsPermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ interface Permissions {
origins: string[];
login: boolean;
operations: boolean;
signPayload: boolean;
}

const clientPermissions: Record<string, Permissions> = {
Expand All @@ -12,6 +13,7 @@ const clientPermissions: Record<string, Permissions> = {
],
login: true,
operations: false,
signPayload: false,
},
};

Expand Down
17 changes: 17 additions & 0 deletions apps/embed-iframe/src/EmbeddedComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand All @@ -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
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -188,6 +204,7 @@ export function EmbeddedComponent() {
<Box className="embedded-component">
{loginModalElement}
{operationModalElement}
{signPayloadModalElement}
</Box>
);
}
2 changes: 1 addition & 1 deletion apps/embed-iframe/src/LoginModalContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
2 changes: 1 addition & 1 deletion apps/embed-iframe/src/OperationModalContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
92 changes: 92 additions & 0 deletions apps/embed-iframe/src/SignPayloadModalContent.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<VStack spacing="0">
<Box marginBottom="10px">
<UmamiLogoIcon />
</Box>

<Heading marginBottom="10px" fontSize="16px" lineHeight="22px">
Sign Payload
</Heading>

<Flex justifyContent="center" marginBottom="16px">
<Text color={color("900")} size="sm" lineHeight="14px" textAlign="center">
<Text as="span">{dAppName ? dAppName : getDAppOrigin()}</Text>
<Text as="span" color={color("500")} marginLeft="5px">
is requesting permission to sign this payload
</Text>
</Text>
</Flex>

<Box
width="100%"
marginBottom="20px"
overflowY="auto"
maxHeight="200px"
padding="16px"
borderRadius="5px"
backgroundColor={color("100")}
>
<Text size="sm">{decodePayload(payload!)}</Text>
</Box>

<LoginButtonComponent
loginType={getUserData()!.typeOfLogin}
prefix="Sign with"
onClick={onClick}
/>
</VStack>
);
};
50 changes: 50 additions & 0 deletions apps/embed-iframe/src/SignPayloadModalContext.tsx
Original file line number Diff line number Diff line change
@@ -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<SignPayloadModalContextState | undefined>(undefined);

export const SignPayloadModalProvider = ({ children }: PropsWithChildren) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [isLoading, setIsLoading] = useState(false);
const [signingType, setSigningType] = useState<SigningType | null>(null);
const [payload, setPayload] = useState<string | null>(null);

return (
<SignPayloadModalContext.Provider
value={{
isOpen,
onOpen,
onClose,
isLoading,
setIsLoading,
signingType,
setSigningType,
payload,
setPayload,
}}
>
{children}
</SignPayloadModalContext.Provider>
);
};

export const useSignPayloadModalContext = (): SignPayloadModalContextState => {
const context = useContext(SignPayloadModalContext);
if (context === undefined) {
throw new Error("useSignPayloadModalContext must be used within a SignPayloadModalProvider");
}
return context;
};
9 changes: 6 additions & 3 deletions apps/embed-iframe/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -18,9 +19,11 @@ ReactDOM.createRoot(rootElement!).render(
<EmbedAppProvider>
<LoginModalProvider>
<OperationModalProvider>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<EmbeddedComponent />
<Analytics />
<SignPayloadModalProvider>
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
<EmbeddedComponent />
<Analytics />
</SignPayloadModalProvider>
</OperationModalProvider>
</LoginModalProvider>
</EmbedAppProvider>
Expand Down
42 changes: 42 additions & 0 deletions apps/embed-iframe/src/signPayloadModalHooks.tsx
Original file line number Diff line number Diff line change
@@ -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: (
<Center>
<Modal
autoFocus={false}
closeOnOverlayClick={false}
isCentered
isOpen={isOpen}
onClose={onClose}
>
<ModalContent>
<ModalCloseButton onClick={onModalCLose} />
<SignPayloadModalContent />
{isLoading && <ModalLoadingOverlay />}
</ModalContent>
</Modal>
</Center>
),
onOpen: (signingType: SigningType, payload: string) => {
setSigningType(signingType);
setPayload(payload);
onOpen();
},
};
};
8 changes: 8 additions & 0 deletions apps/embed-iframe/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down

1 comment on commit f3532d7

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title Lines Statements Branches Functions
apps/desktop Coverage: 83%
83.75% (1763/2105) 78.85% (828/1050) 78.49% (449/572)
apps/web Coverage: 83%
83.75% (1763/2105) 78.85% (828/1050) 78.49% (449/572)
packages/components Coverage: 96%
96.89% (125/129) 98.07% (51/52) 84.21% (32/38)
packages/core Coverage: 84%
84.73% (222/262) 75.93% (101/133) 82.75% (48/58)
packages/crypto Coverage: 100%
100% (28/28) 100% (3/3) 100% (5/5)
packages/data-polling Coverage: 98%
96.55% (140/145) 95.45% (21/22) 92.85% (39/42)
packages/multisig Coverage: 98%
98.4% (123/125) 89.47% (17/19) 100% (33/33)
packages/social-auth Coverage: 100%
100% (21/21) 100% (11/11) 100% (3/3)
packages/state Coverage: 84%
83.98% (771/918) 80.78% (164/203) 78.86% (291/369)
packages/tezos Coverage: 86%
85.57% (89/104) 89.47% (17/19) 82.75% (24/29)
packages/tzkt Coverage: 86%
84.05% (58/69) 81.25% (13/16) 76.92% (30/39)

Please sign in to comment.