From 97fa1988fbe6c10e40a4210556240307b2942623 Mon Sep 17 00:00:00 2001 From: Isha Date: Fri, 8 Nov 2024 12:26:04 +0530 Subject: [PATCH] Keep iframe modal in separate component --- packages/frontend/.env.example | 1 + .../projects/create/AccountsDropdown.tsx | 49 ++++ .../components/projects/create/Configure.tsx | 87 ++++---- .../src/components/projects/create/IFrame.tsx | 124 ----------- .../projects/create/IFrameModal.tsx | 85 +++++++ .../src/context/WalletConnectContext.tsx | 210 ------------------ packages/frontend/src/index.tsx | 23 +- packages/frontend/src/utils/constants.ts | 1 + 8 files changed, 191 insertions(+), 389 deletions(-) create mode 100644 packages/frontend/src/components/projects/create/AccountsDropdown.tsx delete mode 100644 packages/frontend/src/components/projects/create/IFrame.tsx create mode 100644 packages/frontend/src/components/projects/create/IFrameModal.tsx delete mode 100644 packages/frontend/src/context/WalletConnectContext.tsx diff --git a/packages/frontend/.env.example b/packages/frontend/.env.example index 88f6bc1f..75671697 100644 --- a/packages/frontend/.env.example +++ b/packages/frontend/.env.example @@ -15,3 +15,4 @@ VITE_PASSKEY_WALLET_RPID= VITE_TURNKEY_API_BASE_URL= VITE_LACONICD_CHAIN_ID= +VITE_IFRAME_ORIGIN_URL= diff --git a/packages/frontend/src/components/projects/create/AccountsDropdown.tsx b/packages/frontend/src/components/projects/create/AccountsDropdown.tsx new file mode 100644 index 00000000..e404b093 --- /dev/null +++ b/packages/frontend/src/components/projects/create/AccountsDropdown.tsx @@ -0,0 +1,49 @@ +import { Select, Option } from '@snowballtools/material-tailwind-react-fork'; + +const AccountsDropdown = ({ + accounts, + onAccountChange, +}: { + accounts: string[]; + onAccountChange: (selectedAccount: string) => void; +}) => { + + return ( +
+ {!accounts.length ? ( +
+

+ No accounts found. Please visit{' '} + + store.laconic.com + {' '} + to create a wallet. +

+
+ ) : ( +
+ +
+ )} +
+ ); +}; + +export default AccountsDropdown; diff --git a/packages/frontend/src/components/projects/create/Configure.tsx b/packages/frontend/src/components/projects/create/Configure.tsx index 6d29d1de..a8731ba5 100644 --- a/packages/frontend/src/components/projects/create/Configure.tsx +++ b/packages/frontend/src/components/projects/create/Configure.tsx @@ -20,10 +20,11 @@ import { Button } from '../../shared/Button'; import { Input } from 'components/shared/Input'; import { useToast } from 'components/shared/Toast'; import { useGQLClient } from '../../../context/GQLClientContext'; -import IFrame from './IFrame'; +import IFrameModal from './IFrameModal'; import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm'; import { EnvironmentVariablesFormValues } from 'types/types'; -import { VITE_LACONICD_CHAIN_ID } from 'utils/constants'; +import { VITE_LACONICD_CHAIN_ID, VITE_IFRAME_ORIGIN_URL } from 'utils/constants'; +import AccountsDropdown from './AccountsDropdown'; type ConfigureDeploymentFormValues = { option: string; @@ -36,6 +37,7 @@ type ConfigureFormValues = ConfigureDeploymentFormValues & EnvironmentVariablesFormValues; const DEFAULT_MAX_PRICE = '10000'; +const TX_APPROVAL_TIMEOUT = 60000; const Configure = () => { const [isLoading, setIsLoading] = useState(false); @@ -175,7 +177,6 @@ const Configure = () => { const handleFormSubmit = useCallback( async (createFormData: FieldValues) => { try { - setIsFrameVisible(true); const deployerLrn = createFormData.lrn; const deployer = deployers.find( (deployer) => deployer.deployerLrn === deployerLrn, @@ -213,6 +214,33 @@ const Configure = () => { senderAddress, amountToBePaid, ); + + if (!txHash) { + return; + } + + // Validate transaction hash + const isTxHashValid = await verifyTx( + senderAddress, + txHash, + amount, + ); + + if (isTxHashValid) { + toast({ + id: 'payment-successful', + title: 'Payment successful', + variant: 'success', + onDismiss: dismiss, + }); + } else { + toast({ + id: 'invalid-tx-hash', + title: 'Transaction validation failed', + variant: 'error', + onDismiss: dismiss, + }); + } } const environmentVariables = createFormData.variables.map( @@ -300,48 +328,16 @@ const Configure = () => { setIsPaymentDone(false); setIsPaymentLoading(true); - toast({ - id: 'sending-payment-request', - title: 'Check and approve payment request', - variant: 'loading', - onDismiss: dismiss, - }); - await requestTx(senderAddress, snowballAddress, amount); const txHash = await new Promise((resolve, reject) => { const handleTxStatus = async (event: MessageEvent) => { - if (event.origin !== 'http://localhost:3001') return; + if (event.origin !== VITE_IFRAME_ORIGIN_URL) return; if (event.data.type === 'TRANSACTION_SUCCESS') { const txResponse = event.data.data; - + resolve(txResponse); setIsFrameVisible(false); - - // Validate transaction hash - const isTxHashValid = await verifyTx( - senderAddress, - txResponse, - amount, - ); - - if (isTxHashValid) { - resolve(txResponse); - toast({ - id: 'payment-successful', - title: 'Payment successful', - variant: 'success', - onDismiss: dismiss, - }); - } else { - reject(new Error('Invalid transaction hash')); - toast({ - id: 'invalid-tx-hash', - title: 'Transaction validation failed', - variant: 'error', - onDismiss: dismiss, - }); - } } else if (event.data.type === 'ERROR') { console.error('Error from wallet:', event.data.message); reject(new Error('Transaction failed')); @@ -362,7 +358,13 @@ const Configure = () => { setTimeout(() => { reject(new Error('Transaction timeout')); window.removeEventListener('message', handleTxStatus); - }, 60000); + toast({ + id: 'transaction-timeout', + title: 'The transaction request timed out. Please try again', + variant: 'error', + onDismiss: dismiss, + }); + }, TX_APPROVAL_TIMEOUT); }); return txHash; } catch (error) { @@ -391,8 +393,10 @@ const Configure = () => { toAddress: recipient, amount, }, - 'http://localhost:3001' + VITE_IFRAME_ORIGIN_URL ); + + setIsFrameVisible(true); }; useEffect(() => { @@ -561,10 +565,9 @@ const Configure = () => { ) : ( <> - - - - - ); -}; - -export default IFrame; diff --git a/packages/frontend/src/components/projects/create/IFrameModal.tsx b/packages/frontend/src/components/projects/create/IFrameModal.tsx new file mode 100644 index 00000000..b7db68e5 --- /dev/null +++ b/packages/frontend/src/components/projects/create/IFrameModal.tsx @@ -0,0 +1,85 @@ +import { useCallback, useEffect } from 'react'; + +import { Box, Modal } from '@mui/material'; + +import { VITE_LACONICD_CHAIN_ID, VITE_IFRAME_ORIGIN_URL } from 'utils/constants'; + +const IFrameModal = ({ + setAccounts, + isVisible, + toggleModal, +}: { + setAccounts: (accounts: string[]) => void; + isVisible: boolean; + toggleModal: () => void; +}) => { + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + // TODO: Use env for origin URL + if (event.origin !== VITE_IFRAME_ORIGIN_URL) return; + + if (event.data.type === 'WALLET_ACCOUNTS_DATA') { + setAccounts(event.data.data); + } else if (event.data.type === 'ERROR') { + console.error('Error from wallet:', event.data.message); + } + }; + + window.addEventListener('message', handleMessage); + + return () => { + window.removeEventListener('message', handleMessage); + }; + }, []); + + const getDataFromWallet = useCallback(() => { + const iframe = document.getElementById('walletIframe') as HTMLIFrameElement; + + if (!iframe.contentWindow) { + console.error('Iframe not found or not loaded'); + return; + } + + iframe.contentWindow.postMessage( + { + type: 'REQUEST_WALLET_ACCOUNTS', + chainId: VITE_LACONICD_CHAIN_ID, + }, + VITE_IFRAME_ORIGIN_URL + ); + }, []); + + return ( + + + + + + ) +}; + +export default IFrameModal; diff --git a/packages/frontend/src/context/WalletConnectContext.tsx b/packages/frontend/src/context/WalletConnectContext.tsx deleted file mode 100644 index e5dc4fc9..00000000 --- a/packages/frontend/src/context/WalletConnectContext.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import { - createContext, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from 'react'; - -import SignClient from '@walletconnect/sign-client'; -import { getSdkError } from '@walletconnect/utils'; -import { SessionTypes } from '@walletconnect/types'; - -import { walletConnectModal } from '../utils/web3modal'; -import { - VITE_LACONICD_CHAIN_ID, - VITE_WALLET_CONNECT_ID, -} from 'utils/constants'; - -interface ClientInterface { - signClient: SignClient | undefined; - session: SessionTypes.Struct | undefined; - loadingSession: boolean; - onConnect: () => Promise; - onDisconnect: () => Promise; - onSessionDelete: () => void; - accounts: { address: string }[]; -} - -const ClientContext = createContext({} as ClientInterface); - -export const useWalletConnectClient = () => { - return useContext(ClientContext); -}; - -export const WalletConnectClientProvider = ({ - children, -}: { - children: JSX.Element; -}) => { - const [signClient, setSignClient] = useState(); - const [session, setSession] = useState(); - const [loadingSession, setLoadingSession] = useState(true); - const [accounts, setAccounts] = useState<{ address: string }[]>([]); - - const isSignClientInitializing = useRef(false); - - const onSessionConnect = useCallback(async (session: SessionTypes.Struct) => { - setSession(session); - }, []); - - const subscribeToEvents = useCallback( - async (client: SignClient) => { - client.on('session_update', ({ topic, params }) => { - const { namespaces } = params; - const currentSession = client.session.get(topic); - const updatedSession = { ...currentSession, namespaces }; - setSession(updatedSession); - }); - }, - [setSession], - ); - - const onConnect = async () => { - const proposalNamespace = { - cosmos: { - methods: ['cosmos_sendTokens'], - chains: [`cosmos:${VITE_LACONICD_CHAIN_ID}`], - events: [], - }, - }; - - try { - const { uri, approval } = await signClient!.connect({ - requiredNamespaces: proposalNamespace, - }); - - if (uri) { - walletConnectModal.openModal({ uri }); - const session = await approval(); - onSessionConnect(session); - walletConnectModal.closeModal(); - } - } catch (e) { - console.error(e); - } - }; - - const onDisconnect = useCallback(async () => { - if (typeof signClient === 'undefined') { - throw new Error('WalletConnect is not initialized'); - } - if (typeof session === 'undefined') { - throw new Error('Session is not connected'); - } - - await signClient.disconnect({ - topic: session.topic, - reason: getSdkError('USER_DISCONNECTED'), - }); - - onSessionDelete(); - }, [signClient, session]); - - const onSessionDelete = () => { - setAccounts([]); - setSession(undefined); - }; - - const checkPersistedState = useCallback( - async (signClient: SignClient) => { - if (typeof signClient === 'undefined') { - throw new Error('WalletConnect is not initialized'); - } - - if (typeof session !== 'undefined') return; - if (signClient.session.length) { - const lastKeyIndex = signClient.session.keys.length - 1; - const previousSsession = signClient.session.get( - signClient.session.keys[lastKeyIndex], - ); - - await onSessionConnect(previousSsession); - return previousSsession; - } - }, - [session, onSessionConnect], - ); - - const createClient = useCallback(async () => { - isSignClientInitializing.current = true; - try { - const signClient = await SignClient.init({ - projectId: VITE_WALLET_CONNECT_ID, - metadata: { - name: 'Deploy App', - description: '', - url: window.location.href, - icons: ['https://avatars.githubusercontent.com/u/92608123'], - }, - }); - - setSignClient(signClient); - await checkPersistedState(signClient); - await subscribeToEvents(signClient); - setLoadingSession(false); - } catch (e) { - console.error('error in createClient', e); - } - isSignClientInitializing.current = false; - }, [setSignClient, checkPersistedState, subscribeToEvents]); - - useEffect(() => { - if (!signClient && !isSignClientInitializing.current) { - createClient(); - } - }, [signClient, createClient]); - - useEffect(() => { - const populateAccounts = async () => { - if (!session) { - return; - } - if (!session.namespaces['cosmos']) { - console.log('Accounts for cosmos namespace not found'); - return; - } - - const cosmosAddresses = session.namespaces['cosmos'].accounts; - - const cosmosAccounts = cosmosAddresses.map((address) => ({ - address, - })); - - const allAccounts = cosmosAccounts; - - setAccounts(allAccounts); - }; - - populateAccounts(); - }, [session]); - - useEffect(() => { - if (!signClient) { - return; - } - - signClient.on('session_delete', onSessionDelete); - - return () => { - signClient.off('session_delete', onSessionDelete); - }; - }); - - return ( - - {children} - - ); -}; diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx index 787cef61..862eaf67 100644 --- a/packages/frontend/src/index.tsx +++ b/packages/frontend/src/index.tsx @@ -4,10 +4,9 @@ import assert from 'assert'; import { GQLClient } from 'gql-client'; import { ThemeProvider } from '@snowballtools/material-tailwind-react-fork'; - -import './index.css'; import '@fontsource/inter'; import '@fontsource-variable/jetbrains-mono'; + import App from './App'; import reportWebVitals from './reportWebVitals'; import { GQLClientProvider } from './context/GQLClientContext'; @@ -16,7 +15,7 @@ import { Toaster } from 'components/shared/Toast'; import { LogErrorBoundary } from 'utils/log-error'; import { BASE_URL } from 'utils/constants'; import Web3ModalProvider from './context/Web3Provider'; -import { WalletConnectClientProvider } from 'context/WalletConnectContext'; +import './index.css'; console.log(`v-0.0.9`); @@ -32,16 +31,14 @@ const gqlClient = new GQLClient({ gqlEndpoint }); root.render( - - - - - - - - - - + + + + + + + + , ); diff --git a/packages/frontend/src/utils/constants.ts b/packages/frontend/src/utils/constants.ts index 446d9d58..5a0d23ff 100644 --- a/packages/frontend/src/utils/constants.ts +++ b/packages/frontend/src/utils/constants.ts @@ -12,3 +12,4 @@ export const VITE_WALLET_CONNECT_ID = import.meta.env.VITE_WALLET_CONNECT_ID; export const VITE_BUGSNAG_API_KEY = import.meta.env.VITE_BUGSNAG_API_KEY; export const VITE_LIT_RELAY_API_KEY = import.meta.env.VITE_LIT_RELAY_API_KEY; export const VITE_LACONICD_CHAIN_ID = import.meta.env.VITE_LACONICD_CHAIN_ID; +export const VITE_IFRAME_ORIGIN_URL = import.meta.env.VITE_IFRAME_ORIGIN_URL;