Integrate wallet IFrame for payments #42

Merged
nabarun merged 27 commits from iv-integrate-frame into main 2024-11-13 13:32:28 +00:00
21 changed files with 378 additions and 484 deletions

View File

@ -17,6 +17,7 @@ VITE_GITHUB_IMAGE_UPLOAD_PWA_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_image
VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_next_app_templaterepo' VITE_GITHUB_NEXT_APP_TEMPLATE_REPO = 'LACONIC_HOSTED_CONFIG_github_next_app_templaterepo'
VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id' VITE_WALLET_CONNECT_ID = 'LACONIC_HOSTED_CONFIG_wallet_connect_id'
VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id' VITE_LACONICD_CHAIN_ID = 'LACONIC_HOSTED_CONFIG_laconicd_chain_id'
VITE_WALLET_IFRAME_URL = 'LACONIC_HOSTED_CONFIG_wallet_iframe_url'
VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key' VITE_LIT_RELAY_API_KEY = 'LACONIC_HOSTED_CONFIG_lit_relay_api_key'
VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key' VITE_BUGSNAG_API_KEY = 'LACONIC_HOSTED_CONFIG_bugsnag_api_key'
VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid' VITE_PASSKEY_WALLET_RPID = 'LACONIC_HOSTED_CONFIG_passkey_wallet_rpid'

2
packages/deployer/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
records/*
staging-records/*

View File

@ -135,6 +135,7 @@ record:
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15 LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2 LACONIC_HOSTED_CONFIG_laconicd_chain_id: laconic-testnet-2
LACONIC_HOSTED_CONFIG_wallet_iframe_url: https://wallet.laconic.com
meta: meta:
note: Added @ $CURRENT_DATE_TIME note: Added @ $CURRENT_DATE_TIME
repository: "$REPO_URL" repository: "$REPO_URL"

View File

@ -1,18 +0,0 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: deploy-frontend@1.0.0
application: lrn://vaasl/applications/deploy-frontend@1.0.0
dns: deploy
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://deploy-backend.apps.vaasl.io
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liaet4yc0KX0iM1c
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: laconic-templates/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: laconic-templates/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: laconic-templates/starter.nextjs-react-tailwind
LACONIC_HOSTED_CONFIG_wallet_connect_id: 63cad7ba97391f63652161f484670e15
meta:
note: Added by Snowball @ Thu Apr 4 14:49:41 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e

View File

@ -1,8 +0,0 @@
record:
type: ApplicationRecord
version: 0.0.2
repository_ref: 351db16336eacc3e1f9119ceb8d1282b8e27a27e
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: deploy-frontend
app_version: 1.0.0

View File

@ -1,25 +0,0 @@
record:
type: ApplicationDeploymentRequest
version: '1.0.0'
name: staging-snowballtools-base-frontend@0.0.0
application: crn://staging-snowballtools/applications/staging-snowballtools-base-frontend@0.0.0
dns: dashboard.staging.apps.snowballtools.com
config:
env:
LACONIC_HOSTED_CONFIG_server_url: https://snowballtools-base-api.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_github_clientid: Ov23liOaoahRTYd4nSCV
LACONIC_HOSTED_CONFIG_github_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_pwa_templaterepo: snowball-tools/test-progressive-web-app
LACONIC_HOSTED_CONFIG_github_image_upload_templaterepo: snowball-tools/image-upload-pwa-example
LACONIC_HOSTED_CONFIG_github_next_app_templaterepo: snowball-tools/starter.nextjs-react-tailwind
LACONIC_HOSTED_CONFIG_wallet_connect_id: eda9ba18042a5ea500f358194611ece2
LACONIC_HOSTED_CONFIG_lit_relay_api_key: 15DDD969-E75F-404D-AAD9-58A37C4FD354_snowball
LACONIC_HOSTED_CONFIG_aplchemy_api_key: THvPart_gqI5x02RNYSBntlmwA66I_qc
LACONIC_HOSTED_CONFIG_bugsnag_api_key: 8c480cd5386079f9dd44f9581264a073
LACONIC_HOSTED_CONFIG_passkey_wallet_rpid: dashboard.staging.apps.snowballtools.com
LACONIC_HOSTED_CONFIG_turnkey_api_base_url: https://api.turnkey.com
LACONIC_HOSTED_CONFIG_turnkey_organization_id: 5049ae99-5bca-40b3-8317-504384d4e591
meta:
note: Added by Snowball @ Mon Jun 24 23:51:48 UTC 2024
repository: "https://git.vdb.to/cerc-io/snowballtools-base"
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601

View File

@ -1,8 +0,0 @@
record:
type: ApplicationRecord
version: 0.0.1
repository_ref: 61e3e88a6c9d57e95441059369ee5a46f5c07601
repository: ["https://git.vdb.to/cerc-io/snowballtools-base"]
app_type: webapp
name: staging-snowballtools-base-frontend
app_version: 0.0.0

View File

@ -15,3 +15,4 @@ VITE_PASSKEY_WALLET_RPID=
VITE_TURNKEY_API_BASE_URL= VITE_TURNKEY_API_BASE_URL=
VITE_LACONICD_CHAIN_ID= VITE_LACONICD_CHAIN_ID=
VITE_WALLET_IFRAME_URL=

View File

@ -47,6 +47,11 @@ export const ChangeStateToProductionDialog = ({
handleCancel={handleCancel} handleCancel={handleCancel}
open={open} open={open}
handleConfirm={handleConfirm} handleConfirm={handleConfirm}
confirmButtonTitle={
isConfirmButtonLoading
? 'Redeploying'
: 'Redeploy'
}
confirmButtonProps={{ confirmButtonProps={{
disabled: isConfirmButtonLoading, disabled: isConfirmButtonLoading,
rightIcon: isConfirmButtonLoading ? ( rightIcon: isConfirmButtonLoading ? (

View File

@ -0,0 +1,60 @@
import {
Select,
Option,
Spinner,
} from '@snowballtools/material-tailwind-react-fork';
const AccountsDropdown = ({
accounts,
isDataReceived,
onAccountChange,
}: {
accounts: string[];
isDataReceived: boolean;
onAccountChange: (selectedAccount: string) => void;
}) => {
return (
<div className="p-6 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6 shadow-md">
{isDataReceived ? (
!accounts.length ? (
<div className="text-center">
<p className="text-gray-700 dark:text-gray-300 mb-4">
No accounts found. Please visit{' '}
<a
href="https://store.laconic.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 underline dark:text-blue-400"
>
store.laconic.com
</a>{' '}
to create a wallet.
</p>
</div>
) : (
<div>
<Select
label="Select Account"
defaultValue={accounts[0]}
onChange={(value) => value && onAccountChange(value)}
className="dark:bg-overlay2 dark:text-foreground"
aria-label="Wallet Account Selector"
>
{accounts.map((account, index) => (
<Option key={index} value={account}>
{account}
</Option>
))}
</Select>
</div>
)
) : (
<div className="flex items-center justify-center h-12">
<Spinner className="h-6 w-6" />
</div>
)}
</div>
);
};
export default AccountsDropdown;

View File

@ -20,10 +20,14 @@ import { Button } from '../../shared/Button';
import { Input } from 'components/shared/Input'; import { Input } from 'components/shared/Input';
import { useToast } from 'components/shared/Toast'; import { useToast } from 'components/shared/Toast';
import { useGQLClient } from '../../../context/GQLClientContext'; import { useGQLClient } from '../../../context/GQLClientContext';
import IFrameModal from './IFrameModal';
import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm'; import EnvironmentVariablesForm from 'pages/org-slug/projects/id/settings/EnvironmentVariablesForm';
import { EnvironmentVariablesFormValues } from 'types/types'; import { EnvironmentVariablesFormValues } from 'types/types';
import ConnectWallet from './ConnectWallet'; import {
import { useWalletConnectClient } from 'context/WalletConnectContext'; VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL,
} from 'utils/constants';
import AccountsDropdown from './AccountsDropdown';
type ConfigureDeploymentFormValues = { type ConfigureDeploymentFormValues = {
option: string; option: string;
@ -36,16 +40,18 @@ type ConfigureFormValues = ConfigureDeploymentFormValues &
EnvironmentVariablesFormValues; EnvironmentVariablesFormValues;
const DEFAULT_MAX_PRICE = '10000'; const DEFAULT_MAX_PRICE = '10000';
const TX_APPROVAL_TIMEOUT_MS = 60000;
const Configure = () => { const Configure = () => {
const { signClient, session, accounts } = useWalletConnectClient();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [deployers, setDeployers] = useState<Deployer[]>([]); const [deployers, setDeployers] = useState<Deployer[]>([]);
const [selectedAccount, setSelectedAccount] = useState<string>(); const [selectedAccount, setSelectedAccount] = useState<string>();
const [accounts, setAccounts] = useState<string[]>([]);
const [selectedDeployer, setSelectedDeployer] = useState<Deployer>(); const [selectedDeployer, setSelectedDeployer] = useState<Deployer>();
const [isPaymentLoading, setIsPaymentLoading] = useState(false); const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [isPaymentDone, setIsPaymentDone] = useState(false); const [isPaymentDone, setIsPaymentDone] = useState(false);
const [isFrameVisible, setIsFrameVisible] = useState(false);
const [isAccountsDataReceived, setIsAccountsDataReceived] = useState(false);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const templateId = searchParams.get('templateId'); const templateId = searchParams.get('templateId');
@ -182,7 +188,7 @@ const Configure = () => {
let amount: string; let amount: string;
let senderAddress: string; let senderAddress: string;
let txHash: string; let txHash: string | null = null;
if (createFormData.option === 'LRN' && !deployer?.minimumPayment) { if (createFormData.option === 'LRN' && !deployer?.minimumPayment) {
toast({ toast({
id: 'no-payment-required', id: 'no-payment-required',
@ -196,7 +202,7 @@ const Configure = () => {
} else { } else {
if (!selectedAccount) return; if (!selectedAccount) return;
senderAddress = selectedAccount.split(':')[2]; senderAddress = selectedAccount;
if (createFormData.option === 'LRN') { if (createFormData.option === 'LRN') {
amount = deployer?.minimumPayment!; amount = deployer?.minimumPayment!;
@ -208,27 +214,40 @@ const Configure = () => {
const amountToBePaid = amount.replace(/\D/g, '').toString(); const amountToBePaid = amount.replace(/\D/g, '').toString();
const txHashResponse = await cosmosSendTokensHandler( txHash = await cosmosSendTokensHandler(senderAddress, amountToBePaid);
selectedAccount,
amountToBePaid,
);
if (!txHashResponse) { if (!txHash) {
console.error('Tx not successful'); toast({
return; id: 'unsuccessful-tx',
title: 'Transaction rejected',
variant: 'error',
onDismiss: dismiss,
});
setIsFrameVisible(false);
setIsPaymentLoading(false);
throw new Error('Transaction rejected');
} }
txHash = txHashResponse; // Validate transaction hash
const isTxHashValid = await verifyTx(senderAddress, txHash, amountToBePaid);
setIsPaymentLoading(false);
const isTxHashValid = await verifyTx( if (isTxHashValid) {
senderAddress, toast({
txHash, id: 'payment-successful',
amountToBePaid.toString(), title: 'Payment successful',
); variant: 'success',
onDismiss: dismiss,
if (isTxHashValid === false) { });
console.error('Invalid Tx hash', txHash); setIsPaymentDone(true);
return; } else {
toast({
id: 'invalid-tx-hash',
title: 'Transaction validation failed',
variant: 'error',
onDismiss: dismiss,
});
throw new Error('Transaction validation failed');
} }
} }
@ -248,7 +267,7 @@ const Configure = () => {
createFormData, createFormData,
environmentVariables, environmentVariables,
senderAddress, senderAddress,
txHash, txHash!,
); );
await client.getEnvironmentVariables(projectId); await client.getEnvironmentVariables(projectId);
@ -270,14 +289,14 @@ const Configure = () => {
`/${orgSlug}/projects/create/deploy?projectId=${projectId}`, `/${orgSlug}/projects/create/deploy?projectId=${projectId}`,
); );
} }
} catch (error) { } catch (error: any) {
console.error(error);
toast({ toast({
id: 'error-deploying-app', id: 'error-deploying-app',
title: 'Error deploying app', title: 'Error deploying app',
variant: 'error', variant: 'error',
onDismiss: dismiss, onDismiss: dismiss,
}); });
throw new Error(error);
} }
}, },
[client, createProject, dismiss, toast], [client, createProject, dismiss, toast],
@ -302,72 +321,95 @@ const Configure = () => {
const cosmosSendTokensHandler = useCallback( const cosmosSendTokensHandler = useCallback(
async (selectedAccount: string, amount: string) => { async (selectedAccount: string, amount: string) => {
if (!signClient || !session || !selectedAccount) { if (!selectedAccount) {
return; throw new Error('Account not selected');
} }
const chainId = selectedAccount.split(':')[1]; const senderAddress = selectedAccount;
const senderAddress = selectedAccount.split(':')[2];
const snowballAddress = await client.getAddress(); const snowballAddress = await client.getAddress();
let timeoutId;
try { try {
setIsPaymentDone(false); setIsPaymentDone(false);
setIsPaymentLoading(true); setIsPaymentLoading(true);
toast({ await requestTx(senderAddress, snowballAddress, amount);
id: 'sending-payment-request',
title: 'Check your wallet and approve payment request', const txHash = await new Promise<string | null>((resolve, reject) => {
variant: 'loading', const handleTxStatus = async (event: MessageEvent) => {
onDismiss: dismiss, if (event.origin !== VITE_WALLET_IFRAME_URL) return;
if (event.data.type === 'TRANSACTION_RESPONSE') {
const txResponse = event.data.data;
resolve(txResponse);
} else if (event.data.type === 'ERROR') {
console.error('Error from wallet:', event.data.message);
reject(new Error('Transaction failed'));
toast({
id: 'error-transaction',
title: 'Error during transaction',
variant: 'error',
onDismiss: dismiss,
});
}
setIsFrameVisible(false);
window.removeEventListener('message', handleTxStatus);
};
window.addEventListener('message', handleTxStatus);
// Set a timeout, consider unsuccessful after 1 min
timeoutId = setTimeout(() => {
reject(new Error('Transaction timeout'));
window.removeEventListener('message', handleTxStatus);
toast({
id: 'transaction-timeout',
title: 'The transaction request timed out. Please try again',
variant: 'error',
onDismiss: dismiss,
});
setIsFrameVisible(false);
setIsPaymentLoading(false);
}, TX_APPROVAL_TIMEOUT_MS);
}); });
return txHash;
const result: { signature: string } = await signClient.request({ } catch (error) {
topic: session.topic, console.error('Error in transaction:', error);
chainId: `cosmos:${chainId}`, throw new Error('Error in transaction');
request: {
method: 'cosmos_sendTokens',
params: [
{
from: senderAddress,
to: snowballAddress,
value: amount,
},
],
},
});
if (!result) {
throw new Error('Error completing transaction');
}
toast({
id: 'payment-successful',
title: 'Payment successful',
variant: 'success',
onDismiss: dismiss,
});
setIsPaymentDone(true);
return result.signature;
} catch (error: any) {
console.error('Error sending tokens', error);
toast({
id: 'error-sending-tokens',
title: 'Error sending tokens',
variant: 'error',
onDismiss: dismiss,
});
setIsPaymentDone(false);
} finally { } finally {
setIsPaymentLoading(false); clearTimeout(timeoutId);
} }
}, },
[session, signClient, toast], [client, dismiss, toast],
); );
const requestTx = async (
sender: string,
recipient: string,
amount: string,
) => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement;
if (!iframe.contentWindow) {
console.error('Iframe not found or not loaded');
throw new Error('Iframe not found or not loaded');
}
iframe.contentWindow.postMessage(
{
type: 'REQUEST_TX',
chainId: VITE_LACONICD_CHAIN_ID,
fromAddress: sender,
toAddress: recipient,
amount,
},
VITE_WALLET_IFRAME_URL,
);
setIsFrameVisible(true);
};
useEffect(() => { useEffect(() => {
fetchDeployers(); fetchDeployers();
}, []); }, []);
@ -534,10 +576,11 @@ const Configure = () => {
</div> </div>
) : ( ) : (
<> <>
<Heading as="h4" className="md:text-lg font-medium mb-3"> <AccountsDropdown
Connect to your wallet accounts={accounts}
</Heading> onAccountChange={onAccountChange}
<ConnectWallet onAccountChange={onAccountChange} /> isDataReceived={isAccountsDataReceived}
/>
{accounts.length > 0 && ( {accounts.length > 0 && (
<div> <div>
<Button <Button
@ -569,6 +612,12 @@ const Configure = () => {
)} )}
</form> </form>
</FormProvider> </FormProvider>
<IFrameModal
setAccounts={setAccounts}
setIsDataReceived={setIsAccountsDataReceived}
isVisible={isFrameVisible}
/>
</div> </div>
</div> </div>
); );

View File

@ -1,47 +0,0 @@
import { Select, Option } from '@snowballtools/material-tailwind-react-fork';
import { Button } from '../../shared/Button';
import { useWalletConnectClient } from 'context/WalletConnectContext';
const ConnectWallet = ({
onAccountChange,
}: {
onAccountChange: (selectedAccount: string) => void;
}) => {
const { onConnect, accounts } = useWalletConnectClient();
const handleConnect = async () => {
await onConnect();
};
return (
<div className="p-4 bg-slate-100 dark:bg-overlay3 rounded-lg mb-6">
{accounts.length === 0 ? (
<div>
<Button type={'button'} onClick={handleConnect}>
Connect Wallet
</Button>
</div>
) : (
<div>
<Select
label="Select Account"
defaultValue={accounts[0].address}
onChange={(value) => {
value && onAccountChange(value);
}}
className="dark:bg-overlay2 dark:text-foreground"
>
{accounts.map((account, index) => (
<Option key={index} value={account.address}>
{account.address.split(':').slice(1).join(':')}
</Option>
))}
</Select>
</div>
)}
</div>
);
};
export default ConnectWallet;

View File

@ -0,0 +1,88 @@
import { useCallback, useEffect } from 'react';
import { Box, Modal } from '@mui/material';
import {
VITE_LACONICD_CHAIN_ID,
VITE_WALLET_IFRAME_URL,
} from 'utils/constants';
const IFrameModal = ({
setAccounts,
setIsDataReceived,
isVisible,
}: {
setAccounts: (accounts: string[]) => void;
setIsDataReceived: (isReceived: boolean) => void;
isVisible: boolean;
}) => {
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== VITE_WALLET_IFRAME_URL) return;
setIsDataReceived(true);
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_WALLET_IFRAME_URL,
);
}, []);
return (
<Modal open={isVisible} disableEscapeKeyDown keepMounted>
<Box
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '90%',
maxWidth: '1200px',
height: '600px',
maxHeight: '80vh',
overflow: 'auto',
boxShadow: 24,
borderRadius: '8px',
outline: 'none',
bgcolor: 'background.paper',
}}
>
<iframe
onLoad={getDataFromWallet}
id="walletIframe"
src={`${VITE_WALLET_IFRAME_URL}/wallet-embed`}
width="100%"
height="100%"
sandbox="allow-scripts allow-same-origin"
className="border rounded-md shadow-sm"
></iframe>
</Box>
</Modal>
);
};
export default IFrameModal;

View File

@ -4,10 +4,7 @@ import { cloneIcon } from 'utils/cloneIcon';
import { PWAIcon } from './PWAIcon'; import { PWAIcon } from './PWAIcon';
import { WebAppIcon } from './WebAppIcon'; import { WebAppIcon } from './WebAppIcon';
const TEMPLATE_ICONS = [ const TEMPLATE_ICONS = ['pwa', 'web'] as const;
'pwa',
'web'
] as const;
export type TemplateIconType = (typeof TEMPLATE_ICONS)[number]; export type TemplateIconType = (typeof TEMPLATE_ICONS)[number];
export interface TemplateIconProps extends CustomIconProps { export interface TemplateIconProps extends CustomIconProps {

View File

@ -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<void>;
onDisconnect: () => Promise<void>;
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<SignClient>();
const [session, setSession] = useState<SessionTypes.Struct>();
const [loadingSession, setLoadingSession] = useState(true);
const [accounts, setAccounts] = useState<{ address: string }[]>([]);
const isSignClientInitializing = useRef<boolean>(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 (
<ClientContext.Provider
value={{
signClient,
onConnect,
onDisconnect,
onSessionDelete,
loadingSession,
session,
accounts,
}}
>
{children}
</ClientContext.Provider>
);
};

View File

@ -4,10 +4,9 @@ import assert from 'assert';
import { GQLClient } from 'gql-client'; import { GQLClient } from 'gql-client';
import { ThemeProvider } from '@snowballtools/material-tailwind-react-fork'; import { ThemeProvider } from '@snowballtools/material-tailwind-react-fork';
import './index.css';
import '@fontsource/inter'; import '@fontsource/inter';
import '@fontsource-variable/jetbrains-mono'; import '@fontsource-variable/jetbrains-mono';
import App from './App'; import App from './App';
import reportWebVitals from './reportWebVitals'; import reportWebVitals from './reportWebVitals';
import { GQLClientProvider } from './context/GQLClientContext'; import { GQLClientProvider } from './context/GQLClientContext';
@ -16,7 +15,7 @@ import { Toaster } from 'components/shared/Toast';
import { LogErrorBoundary } from 'utils/log-error'; import { LogErrorBoundary } from 'utils/log-error';
import { BASE_URL } from 'utils/constants'; import { BASE_URL } from 'utils/constants';
import Web3ModalProvider from './context/Web3Provider'; import Web3ModalProvider from './context/Web3Provider';
import { WalletConnectClientProvider } from 'context/WalletConnectContext'; import './index.css';
console.log(`v-0.0.9`); console.log(`v-0.0.9`);
@ -32,16 +31,14 @@ const gqlClient = new GQLClient({ gqlEndpoint });
root.render( root.render(
<LogErrorBoundary> <LogErrorBoundary>
<React.StrictMode> <React.StrictMode>
<WalletConnectClientProvider> <ThemeProvider>
<ThemeProvider> <Web3ModalProvider>
<Web3ModalProvider> <GQLClientProvider client={gqlClient}>
<GQLClientProvider client={gqlClient}> <App />
<App /> <Toaster />
<Toaster /> </GQLClientProvider>
</GQLClientProvider> </Web3ModalProvider>
</Web3ModalProvider> </ThemeProvider>
</ThemeProvider>
</WalletConnectClientProvider>
</React.StrictMode> </React.StrictMode>
</LogErrorBoundary>, </LogErrorBoundary>,
); );

View File

@ -60,9 +60,9 @@ const Id = () => {
fetchProject(id); fetchProject(id);
}, [id]); }, [id]);
const onUpdate = async () => { const onUpdate = useCallback(async () => {
await fetchProject(id); await fetchProject(id);
}; }, [fetchProject, id]);
return ( return (
<div className="h-full"> <div className="h-full">

View File

@ -1,12 +1,14 @@
import { ComponentPropsWithoutRef } from 'react'; import { ComponentPropsWithoutRef } from 'react';
import { Link, Outlet, useParams } from 'react-router-dom'; import { Link, Outlet, useParams } from 'react-router-dom';
import { useMediaQuery } from 'usehooks-ts';
import * as Dialog from '@radix-ui/react-dialog';
import { Heading } from 'components/shared/Heading'; import { Heading } from 'components/shared/Heading';
import { WavyBorder } from 'components/shared/WavyBorder'; import { WavyBorder } from 'components/shared/WavyBorder';
import { Button } from 'components/shared/Button'; import { Button } from 'components/shared/Button';
import { CrossIcon } from 'components/shared/CustomIcon'; import { CrossIcon } from 'components/shared/CustomIcon';
import { cn } from 'utils/classnames'; import { cn } from 'utils/classnames';
import * as Dialog from '@radix-ui/react-dialog';
export interface CreateProjectLayoutProps export interface CreateProjectLayoutProps
extends ComponentPropsWithoutRef<'section'> {} extends ComponentPropsWithoutRef<'section'> {}
@ -16,6 +18,7 @@ export const CreateProjectLayout = ({
...props ...props
}: CreateProjectLayoutProps) => { }: CreateProjectLayoutProps) => {
const { orgSlug } = useParams(); const { orgSlug } = useParams();
const isDesktopView = useMediaQuery('(min-width: 720px)'); // md:
const closeBtnLink = `/${orgSlug}`; const closeBtnLink = `/${orgSlug}`;
@ -28,72 +31,69 @@ export const CreateProjectLayout = ({
</Heading> </Heading>
); );
return ( return isDesktopView ? (
<> // Desktop
{/* Desktop */} <section
<section {...props}
{...props} className={cn(
className={cn( 'dark:bg-background h-full flex-col hidden md:flex',
'dark:bg-background h-full flex-col hidden md:flex', className,
className, )}
)} >
> <div className="sticky top-0">
<div className="sticky top-0"> <div className="flex px-6 py-4 dark:bg-overlay bg-base-bg items-center gap-4">
<div className="flex px-6 py-4 dark:bg-overlay bg-base-bg items-center gap-4"> {heading}
{heading}
{/* Cannot save btn as variable since responsive variant don't work with compoundVariant */} {/* Cannot save btn as variable since responsive variant don't work with compoundVariant */}
<Link to={closeBtnLink}> <Link to={closeBtnLink}>
<Button <Button
iconOnly iconOnly
variant="primary" variant="primary"
leftIcon={<CrossIcon />} leftIcon={<CrossIcon />}
aria-label="close" aria-label="close"
/> />
</Link> </Link>
</div>
<WavyBorder />
</div> </div>
<WavyBorder />
<section className="px-6 h-full flex-1 py-6 overflow-y-auto"> </div>
<Outlet /> <section className="px-6 h-full flex-1 py-6 overflow-y-auto">
</section> <Outlet />
</section> </section>
</section>
) : (
// Mobile
// Setting modal={false} so even if modal is active on desktop, it doesn't block clicks
<Dialog.Root modal={false} open={true}>
<Dialog.Portal>
{/* Not using <Dialog.Overlay> since modal={false} disables it and its content will not show */}
<div className="bg-base-canvas fixed inset-0 md:hidden overflow-y-auto p-1">
<Dialog.Content className="min-h-full overflow-hidden rounded-2xl bg-base-bg shadow-card focus:outline-none">
{/* Heading */}
<div className="flex px-6 py-4 h-20 items-center gap-4">
{heading}
<Dialog.Close asChild>
<Link to={closeBtnLink}>
<Button
iconOnly
variant="tertiary"
leftIcon={<CrossIcon />}
aria-label="close"
size="sm"
/>
</Link>
</Dialog.Close>
</div>
{/* Mobile */} {/* Border */}
{/* Setting modal={false} so even if modal is active on desktop, it doesn't block clicks */} <WavyBorder />
<Dialog.Root modal={false} open={true}>
<Dialog.Portal>
{/* Not using <Dialog.Overlay> since modal={false} disables it and its content will not show */}
<div className="bg-base-canvas fixed inset-0 md:hidden overflow-y-auto p-1">
<Dialog.Content className="min-h-full overflow-hidden rounded-2xl bg-base-bg shadow-card focus:outline-none">
{/* Heading */}
<div className="flex px-6 py-4 h-20 items-center gap-4">
{heading}
<Dialog.Close asChild>
<Link to={closeBtnLink}>
<Button
iconOnly
variant="tertiary"
leftIcon={<CrossIcon />}
aria-label="close"
size="sm"
/>
</Link>
</Dialog.Close>
</div>
{/* Border */} {/* Page content */}
<WavyBorder /> <div className="px-4 py-6">
<Outlet />
{/* Page content */} </div>
<div className="px-4 py-6"> </Dialog.Content>
<Outlet /> </div>
</div> </Dialog.Portal>
</Dialog.Content> </Dialog.Root>
</div>
</Dialog.Portal>
</Dialog.Root>
</>
); );
}; };

View File

@ -24,6 +24,7 @@ import { Domain, DomainStatus } from 'gql-client';
import { AuctionCard } from 'components/projects/project/overview/Activity/AuctionCard'; import { AuctionCard } from 'components/projects/project/overview/Activity/AuctionCard';
const COMMITS_PER_PAGE = 4; const COMMITS_PER_PAGE = 4;
const PROJECT_UPDATE_WAIT_MS = 5000;
const OverviewTabPanel = () => { const OverviewTabPanel = () => {
const { octokit } = useOctokit(); const { octokit } = useOctokit();
@ -33,8 +34,7 @@ const OverviewTabPanel = () => {
const [liveDomain, setLiveDomain] = useState<Domain>(); const [liveDomain, setLiveDomain] = useState<Domain>();
const client = useGQLClient(); const client = useGQLClient();
const { project, onUpdate } = useOutletContext<OutletContextType>();
const { project } = useOutletContext<OutletContextType>();
useEffect(() => { useEffect(() => {
setFetchingActivities(true); setFetchingActivities(true);
@ -96,7 +96,16 @@ const OverviewTabPanel = () => {
}; };
fetchRepoActivity(); fetchRepoActivity();
}, [octokit, project]); }, [project.repository]);
useEffect(() => {
onUpdate();
const timerId = setInterval(() => {
onUpdate();
}, PROJECT_UPDATE_WAIT_MS);
return () => clearInterval(timerId);
}, [onUpdate]);
useEffect(() => { useEffect(() => {
const fetchLiveProdDomain = async () => { const fetchLiveProdDomain = async () => {

View File

@ -18,7 +18,6 @@ const GeneralTabPanel = () => {
const client = useGQLClient(); const client = useGQLClient();
const { toast } = useToast(); const { toast } = useToast();
const { project, onUpdate } = useOutletContext<OutletContextType>(); const { project, onUpdate } = useOutletContext<OutletContextType>();
console.log(project);
const [transferOrganizations, setTransferOrganizations] = useState< const [transferOrganizations, setTransferOrganizations] = useState<
SelectOption[] SelectOption[]

View File

@ -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_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_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_LACONICD_CHAIN_ID = import.meta.env.VITE_LACONICD_CHAIN_ID;
export const VITE_WALLET_IFRAME_URL = import.meta.env.VITE_WALLET_IFRAME_URL;