diff --git a/examples/hello/frontend/index.html b/examples/hello/frontend/index.html index 1b2a3726..e5a4de8b 100644 --- a/examples/hello/frontend/index.html +++ b/examples/hello/frontend/index.html @@ -4,12 +4,6 @@ - - - Universal Hello diff --git a/examples/hello/frontend/package.json b/examples/hello/frontend/package.json index d70e086f..0ccc7468 100644 --- a/examples/hello/frontend/package.json +++ b/examples/hello/frontend/package.json @@ -10,8 +10,8 @@ "preview": "vite preview" }, "dependencies": { - "@tanstack/react-query": "^5.83.0", "@zetachain/toolkit": "16.0.1", + "clsx": "^2.1.1", "ethers": "^6.13.2", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/examples/hello/frontend/public/fonts/inter/OFL.txt b/examples/hello/frontend/public/fonts/inter/OFL.txt new file mode 100644 index 00000000..ad214842 --- /dev/null +++ b/examples/hello/frontend/public/fonts/inter/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/hello/frontend/public/fonts/inter/inter-black.ttf b/examples/hello/frontend/public/fonts/inter/inter-black.ttf new file mode 100644 index 00000000..6bb38b6f Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-black.ttf differ diff --git a/examples/hello/frontend/public/fonts/inter/inter-bold.ttf b/examples/hello/frontend/public/fonts/inter/inter-bold.ttf new file mode 100644 index 00000000..90623289 Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-bold.ttf differ diff --git a/examples/hello/frontend/public/fonts/inter/inter-extra-bold.ttf b/examples/hello/frontend/public/fonts/inter/inter-extra-bold.ttf new file mode 100644 index 00000000..c746904e Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-extra-bold.ttf differ diff --git a/examples/hello/frontend/public/fonts/inter/inter-extra-light.ttf b/examples/hello/frontend/public/fonts/inter/inter-extra-light.ttf new file mode 100644 index 00000000..1c92afea Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-extra-light.ttf differ diff --git a/examples/hello/frontend/public/fonts/inter/inter-light.ttf b/examples/hello/frontend/public/fonts/inter/inter-light.ttf new file mode 100644 index 00000000..3e1cd324 Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-light.ttf differ diff --git a/examples/hello/frontend/public/fonts/inter/inter-medium.ttf b/examples/hello/frontend/public/fonts/inter/inter-medium.ttf new file mode 100644 index 00000000..49b53ab3 Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-medium.ttf differ diff --git a/examples/hello/frontend/public/fonts/inter/inter-regular.ttf b/examples/hello/frontend/public/fonts/inter/inter-regular.ttf new file mode 100644 index 00000000..f17b596c Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-regular.ttf differ diff --git a/examples/hello/frontend/public/fonts/inter/inter-semi-bold.ttf b/examples/hello/frontend/public/fonts/inter/inter-semi-bold.ttf new file mode 100644 index 00000000..01523b22 Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-semi-bold.ttf differ diff --git a/examples/hello/frontend/public/fonts/inter/inter-thin.ttf b/examples/hello/frontend/public/fonts/inter/inter-thin.ttf new file mode 100644 index 00000000..089857e4 Binary files /dev/null and b/examples/hello/frontend/public/fonts/inter/inter-thin.ttf differ diff --git a/examples/hello/frontend/src/App.tsx b/examples/hello/frontend/src/App.tsx index 1044b55d..678929b6 100644 --- a/examples/hello/frontend/src/App.tsx +++ b/examples/hello/frontend/src/App.tsx @@ -1,12 +1,13 @@ import { AppContent } from './AppContent'; import { Header } from './components/Header'; +import { ThemeProvider } from './context/ThemeProvider'; function App() { return ( - <> +
- + ); } diff --git a/examples/hello/frontend/src/AppContent.tsx b/examples/hello/frontend/src/AppContent.tsx index 1fada52d..2cb33dba 100644 --- a/examples/hello/frontend/src/AppContent.tsx +++ b/examples/hello/frontend/src/AppContent.tsx @@ -1,39 +1,19 @@ -import { useMemo } from 'react'; - import { ConnectedContent } from './ConnectedContent'; import { SUPPORTED_CHAINS } from './constants/chains'; import { DisconnectedContent } from './DisconnectedContent'; import { useWallet } from './hooks/useWallet'; -import { UnsupportedNetworkContent } from './UnsupportedNetworkContent'; export function AppContent() { - const { - isConnected, - account, - selectedProvider, - isSupportedChain, - decimalChainId, - } = useWallet(); + const { account, selectedProvider, decimalChainId } = useWallet(); const supportedChain = SUPPORTED_CHAINS.find( (chain) => chain.chainId === decimalChainId ); - const shouldDisplayUnsupportedChainWarning = useMemo(() => { - return isConnected && !isSupportedChain && decimalChainId !== null; - }, [isConnected, isSupportedChain, decimalChainId]); - if (!account || !selectedProvider) { return ; } - if (shouldDisplayUnsupportedChainWarning || !supportedChain) { - return ( - // Type assertion is safe because we know decimalChainId is not null - - ); - } - return ( div { +.confirmed-content-links-container { align-items: center; display: flex; - gap: 0.5rem; + flex-wrap: wrap; justify-content: center; - - p, - a { - text-align: center; - white-space: nowrap; - } + gap: 16px; } -.transaction-hash-link { +.confirmed-content-link-chain { align-items: center; - color: var(--primary-200); display: flex; - gap: 0.25rem; + gap: 4px; +} - svg { - color: var(--primary-200); - } +.confirmed-content-link { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 20.8px */ + text-align: center; + white-space: nowrap; } -.transaction-hash-status { - font-size: 1.5rem; +.confirmed-content-link-disabled { + color: #848484; + pointer-events: none; + text-decoration: none; +} + +[data-theme='light'] .confirmed-content { + background: #ffffffcc; + border: 1px solid #e5e8ec; +} + +[data-theme='dark'] .confirmed-content { + background: #171f29cc; + border: 1px solid #283442; +} + +[data-theme='light'] .confirmed-content-link-enabled { + color: #00a87d; +} + +[data-theme='dark'] .confirmed-content-link-enabled { + color: #b0ff61; } @media (min-width: 768px) { - .transaction-hash-container { - flex-direction: row; + .confirmed-content { + padding: 48px 32px; + width: 568px; + } + + .confirmed-content-title { + font-size: 24px; + } + + .confirmed-content-link { + font-size: 16px; } } diff --git a/examples/hello/frontend/src/ConfirmedContent.tsx b/examples/hello/frontend/src/ConfirmedContent.tsx index a79bf15b..a656a1e4 100644 --- a/examples/hello/frontend/src/ConfirmedContent.tsx +++ b/examples/hello/frontend/src/ConfirmedContent.tsx @@ -1,53 +1,43 @@ import './ConfirmedContent.css'; +import clsx from 'clsx'; import { useEffect, useMemo, useState } from 'react'; -import { IconExternalLink } from './components/IconExternalLink'; +import { Button } from './components/Button'; +import { IconReceived, IconSpinner } from './components/icons'; import { type SupportedChain, ZETACHAIN_ATHENS_BLOCKSCOUT_EXPLORER_URL, } from './constants/chains'; -import { - type CrosschainCctxStatus, - crosschainCctxStatus, - type CrossChainTxResponse, -} from './types/cctx'; +import { type CrossChainTxResponse } from './types/cctx'; import type { EIP6963ProviderDetail } from './types/wallet'; -import { truncateAddress } from './utils/truncate'; const CCTX_POLLING_URL = 'https://zetachain-athens.blockpi.network/lcd/v1/public/zeta-chain/crosschain/inboundHashToCctxData'; interface ConfirmedContentProps { selectedProvider: EIP6963ProviderDetail; - supportedChain: SupportedChain; + supportedChain: SupportedChain | undefined; connectedChainTxHash: string; - connectedChainTxResult: number | null; handleSendAnotherMessage: () => void; + stringValue: string; } +const MAX_STRING_LENGTH = 20; + export function ConfirmedContent({ supportedChain, connectedChainTxHash, - connectedChainTxResult, handleSendAnotherMessage, + stringValue, }: ConfirmedContentProps) { - const isConnectedChainTxSuccessful = connectedChainTxResult === 1; const [zetachainTxHash, setZetachainTxHash] = useState(null); - const [zetachainTxStatus, setZetachainTxStatus] = - useState(null); - - const zetachainTxIcon = useMemo(() => { - switch (zetachainTxStatus) { - case crosschainCctxStatus.OutboundMined: - return '✅'; - case crosschainCctxStatus.Aborted: - case crosschainCctxStatus.Reverted: - return '❌'; - default: - return '⏳'; + const renderString = useMemo(() => { + if (stringValue.length > MAX_STRING_LENGTH) { + return stringValue.slice(0, MAX_STRING_LENGTH) + '...'; } - }, [zetachainTxStatus]); + return stringValue; + }, [stringValue]); // Poll for the ZetaChain transaction status every 15 seconds useEffect(() => { @@ -65,9 +55,6 @@ export function ConfirmedContent({ const txHash = data.CrossChainTxs?.[0]?.outbound_params?.[0]?.hash; if (txHash) { setZetachainTxHash(txHash); - setZetachainTxStatus( - data.CrossChainTxs[0]?.cctx_status?.status as CrosschainCctxStatus - ); } } } catch (error) { @@ -84,60 +71,56 @@ export function ConfirmedContent({ }, [connectedChainTxHash, zetachainTxHash]); return ( -
-

{zetachainTxHash ? 'Hello tx arrived!' : 'Hello in transit...'}

-
-
- - {isConnectedChainTxSuccessful ? '✅' : '❌'} - -

{supportedChain.name} Transaction:

-
- -
-
- {zetachainTxHash ? ( - <> -
- {zetachainTxIcon} -

ZetaChain Transaction:

-
- - - ) : ( -

⏳ Waiting for ZetaChain transaction...

+
+ +

+ "{renderString}" {!zetachainTxHash ? 'in Transit' : 'Received'} +

+
+ {supportedChain && ( +
+ {!connectedChainTxHash && } + + View on {supportedChain.name} + +
+ )} + {connectedChainTxHash && ( +
+ {!zetachainTxHash && } + + View on ZetaChain + +
)}
- + Send Another +
); } diff --git a/examples/hello/frontend/src/ConnectedContent.css b/examples/hello/frontend/src/ConnectedContent.css index 2f91b6b5..50cae796 100644 --- a/examples/hello/frontend/src/ConnectedContent.css +++ b/examples/hello/frontend/src/ConnectedContent.css @@ -1,19 +1,45 @@ -.input-container { +.content-container { align-items: center; display: flex; flex-direction: column; - gap: 1rem; - justify-content: center; - width: 60vw; + gap: 64px; } -.input-container-inner { - align-items: center; - background-color: var(--primary-light); +.content-container-inner { display: flex; flex-direction: column; - /* justify-content: space-between; */ - width: 100%; + gap: 24px; + margin-top: 128px; +} + +.content-container-inner-header { + display: flex; + flex-direction: column; + gap: 16px; +} + +.content-container-inner h1 { + margin: 0; + font-size: 42px; + white-space: nowrap; + font-weight: 700; + line-height: 100%; /* 80px */ + letter-spacing: -1.6px; + text-align: left; +} + +.content-container-inner-description { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 160%; + text-align: left; + + .highlight { + border-radius: 8px; + font-weight: 600; + padding: 4px 12px; + } } .call-input { @@ -42,7 +68,42 @@ font-size: 1.5rem; } -@media (min-width: 768px) { +[data-theme='light'] .content-container-inner-description { + color: #696e75; + + .highlight { + background: #eff1f4; + } +} + +[data-theme='dark'] .content-container-inner-description { + color: #a9acb0; + + .highlight { + background: #26303d; + } +} + +@media (min-width: 1168px) { + .content-container { + flex-direction: row; + gap: 32px; + min-height: 65vh; + } + + .content-container-inner { + flex: 1; + margin-top: 0; + + h1 { + font-size: 76px; + } + } + + .content-container-inner-description { + font-size: 18px; + } + .input-container-inner { align-items: center; display: flex; diff --git a/examples/hello/frontend/src/ConnectedContent.tsx b/examples/hello/frontend/src/ConnectedContent.tsx index d2cc827f..c14d5cec 100644 --- a/examples/hello/frontend/src/ConnectedContent.tsx +++ b/examples/hello/frontend/src/ConnectedContent.tsx @@ -1,146 +1,51 @@ import './ConnectedContent.css'; -import { evmCall } from '@zetachain/toolkit/chains/evm'; -import { ethers, ZeroAddress } from 'ethers'; -import { useState } from 'react'; - -import { ConfirmedContent } from './ConfirmedContent'; +import { NetworkSelector } from './components/NetworkSelector'; import type { SupportedChain } from './constants/chains'; -import { HELLO_UNIVERSAL_CONTRACT_ADDRESS } from './constants/contracts'; +import { Footer } from './Footer'; +import { useSwitchChain } from './hooks/useSwitchChain'; +import { MessageFlowCard } from './MessageFlowCard'; import type { EIP6963ProviderDetail } from './types/wallet'; interface ConnectedContentProps { selectedProvider: EIP6963ProviderDetail; - supportedChain: SupportedChain; + supportedChain: SupportedChain | undefined; } export function ConnectedContent({ selectedProvider, supportedChain, }: ConnectedContentProps) { - const MAX_STRING_LENGTH = 20; - const [isUserSigningTx, setIsUserSigningTx] = useState(false); - const [isTxReceiptLoading, setIsTxReceiptLoading] = useState(false); - const [stringValue, setStringValue] = useState(''); - const [connectedChainTxHash, setConnectedChainTxHash] = useState(''); - const [connectedChainTxResult, setConnectedChainTxResult] = useState< - number | null - >(null); - - const handleEvmCall = async () => { - try { - const ethersProvider = new ethers.BrowserProvider( - selectedProvider.provider - ); - const signer = - (await ethersProvider.getSigner()) as ethers.AbstractSigner; - - const evmCallParams = { - receiver: HELLO_UNIVERSAL_CONTRACT_ADDRESS, - types: ['string'], - values: [stringValue], - revertOptions: { - callOnRevert: false, - revertAddress: ZeroAddress, - revertMessage: '', - abortAddress: ZeroAddress, - onRevertGasLimit: 1000000, - }, - }; - - const evmCallOptions = { - signer, - txOptions: { - gasLimit: 1000000, - }, - }; - - setIsUserSigningTx(true); - - const result = await evmCall(evmCallParams, evmCallOptions); - - setIsTxReceiptLoading(true); + const { switchChain } = useSwitchChain(); - const receipt = await result.wait(); - - setConnectedChainTxHash(result.hash); - setConnectedChainTxResult(receipt?.status ?? null); - } catch (error) { - console.error(error); - } finally { - setIsUserSigningTx(false); - setIsTxReceiptLoading(false); - } + const handleNetworkSelect = (chain: SupportedChain) => { + switchChain(chain.chainId); }; - if (isUserSigningTx) { - return ( -
-

- {isTxReceiptLoading - ? `Waiting for transaction receipt on ${supportedChain.name}...` - : 'Sign transaction in your wallet...'} -

-
- ); - } - - if (connectedChainTxHash) { - return ( - { - setConnectedChainTxHash(''); - setStringValue(''); - setConnectedChainTxResult(null); - }} - /> - ); - } - return (
-

Ready to say "Hello" on: {supportedChain.name}

-

- This transaction will emit a cross-chain "HelloEvent" event on ZetaChain - testnet's Universal Hello contract. -

- {isUserSigningTx ? ( -
-

Signing transaction...

-
- ) : ( -
-
- { - if (e.target.value.length <= MAX_STRING_LENGTH) { - setStringValue(e.target.value); - } - }} +
+
+
+

Say Hello from

+ -
- - {stringValue.length} / {MAX_STRING_LENGTH} characters - +

+ Make a cross-chain call with a message from{' '} + {supportedChain?.name || 'a supported network'} to a universal + contract on ZetaChain that emits a{' '} + HelloEvent. +

- )} + +
+
); } diff --git a/examples/hello/frontend/src/DisconnectedContent.css b/examples/hello/frontend/src/DisconnectedContent.css new file mode 100644 index 00000000..4c827b98 --- /dev/null +++ b/examples/hello/frontend/src/DisconnectedContent.css @@ -0,0 +1,102 @@ +.hero-content { + align-items: flex-start; + display: flex; + flex-direction: column; + gap: 32px; + justify-content: center; +} + +.hero-content-container { + display: flex; + flex-direction: column-reverse; + gap: 32px; + min-height: 65vh; +} + +.hero-content-header { + display: flex; + flex-direction: column; + gap: 8px; +} + +.hero-content-header-title { + font-size: 48px; + font-style: normal; + font-weight: 700; + letter-spacing: -1.6px; + line-height: 100%; + text-align: left; +} + +.hero-content-header-logo { + align-items: center; + display: flex; + gap: 8px; +} + +.hero-content-header-logo-text { + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 130%; +} + +[data-theme='light'] .hero-content-header-logo-text, +.hero-content-description { + color: var(--text-secondary-light); +} + +[data-theme='dark'] .hero-content-header-logo-text, +.hero-content-description { + color: var(--text-secondary-dark); +} + +.hero-content-description { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 160%; + text-align: left; +} + +.hero-content-animation { + display: flex; + justify-content: center; + align-items: center; +} + +@media (min-width: 768px) { + .hero-content { + gap: 24px; + } + + .hero-content-container { + flex-direction: row; + } + + .hero-content-header { + gap: 16px; + } + + .hero-content-header-title { + font-size: 56px; + } + + .hero-content-description { + font-size: 18px; + } + + .hero-content-animation { + min-width: 380px; + } +} + +@media (min-width: 1168px) { + .hero-content-header-title { + font-size: 80px; + } + + .hero-content-animation { + min-width: 568px; + } +} diff --git a/examples/hello/frontend/src/DisconnectedContent.tsx b/examples/hello/frontend/src/DisconnectedContent.tsx index 4f6af34b..35552301 100644 --- a/examples/hello/frontend/src/DisconnectedContent.tsx +++ b/examples/hello/frontend/src/DisconnectedContent.tsx @@ -1,45 +1,33 @@ -import { useState } from 'react'; +import './DisconnectedContent.css'; -import { WalletSelectionModal } from './components/WalletSelectionModal'; -import { useWallet } from './hooks/useWallet'; -import type { EIP6963ProviderDetail } from './types/wallet'; +import { ConnectWallet } from './components/ConnectWallet'; +import { IconAnimation } from './components/icons/IconAnimation'; +import { IconZetaChainLogo } from './components/icons/IconZetaChainLogo'; +import { Footer } from './Footer'; export function DisconnectedContent() { - const [isModalOpen, setIsModalOpen] = useState(false); - const { providers, connectWallet, error, connecting } = useWallet(); - - const handleConnectClick = () => { - if (providers.length > 0) { - setIsModalOpen(true); - } else { - alert('No wallet providers found. Please install a wallet extension.'); - } - }; - - const handleSelectProvider = (provider: EIP6963ProviderDetail) => { - connectWallet(provider); - setIsModalOpen(false); - }; - return (
-

Say "Hello" cross-chain!

-

- Connect your EVM wallet and trigger the Universal Hello contract already - live on ZetaChain testnet from any of our supported EVM chains. -

-
- {error &&

Error: {error}

} - +
+
+
+

Call a Universal App

+
+ from + +
+
+

+ Connect your EVM wallet and trigger the Universal Hello contract on + ZetaChain testnet from any currently supported EVM chain. +

+ +
+
+ +
- setIsModalOpen(false)} - providers={providers} - onConnect={handleSelectProvider} - /> +
); } diff --git a/examples/hello/frontend/src/Footer.css b/examples/hello/frontend/src/Footer.css new file mode 100644 index 00000000..a3676e3f --- /dev/null +++ b/examples/hello/frontend/src/Footer.css @@ -0,0 +1,88 @@ +.footer-container { + align-items: flex-start; + align-self: stretch; + display: flex; + flex-direction: column; + gap: 16px; +} + +.footer-card { + align-items: center; + border-radius: 8px; + display: flex; + gap: 16px; + padding: 24px 16px; + width: 100%; + + svg:first-child { + border-radius: 6px; + height: 40px; + width: 40px; + flex-shrink: 0; + } +} + +.footer-card-text { + align-items: flex-start; + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; +} + +.footer-card-text-title { + font-size: 20px; + font-style: normal; + font-weight: 500; + letter-spacing: -0.48px; + line-height: 110%; /* 26.4px */ + white-space: nowrap; +} + +.footer-card-text-subtitle { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 130%; /* 20.8px */ +} + +[data-theme='light'] .footer-card { + border: 1px solid #e5e8ec; +} + +[data-theme='dark'] .footer-card { + border: 1px solid #283442; +} + +[data-theme='light'] .footer-card-text-subtitle { + color: var(--text-secondary-light); +} + +[data-theme='dark'] .footer-card-text-subtitle { + color: var(--text-secondary-dark); +} + +@media (min-width: 1068px) { + .footer-container { + flex-direction: row; + gap: 32px; + } + + .footer-card { + flex: 1; + padding: 32px 24px; + + svg:first-child { + height: 48px; + width: 48px; + } + } + + .footer-card-text-title { + font-size: 24px; + } + + .footer-card-text-subtitle { + font-size: 16px; + } +} diff --git a/examples/hello/frontend/src/Footer.tsx b/examples/hello/frontend/src/Footer.tsx new file mode 100644 index 00000000..28b7e38b --- /dev/null +++ b/examples/hello/frontend/src/Footer.tsx @@ -0,0 +1,54 @@ +import './Footer.css'; + +import { + IconArrowRotated, + IconDiscuss, + IconDocs, + IconTutorials, +} from './components/icons'; + +export function Footer() { + return ( + + ); +} diff --git a/examples/hello/frontend/src/MessageFlowCard.css b/examples/hello/frontend/src/MessageFlowCard.css new file mode 100644 index 00000000..90b9d478 --- /dev/null +++ b/examples/hello/frontend/src/MessageFlowCard.css @@ -0,0 +1,214 @@ +.message-flow-container { + align-items: flex-start; + backdrop-filter: blur(12px); + border-radius: 40px; + display: flex; + flex-direction: column; + height: fit-content; + padding: 32px; + width: 100%; +} + +.approve-container { + align-items: center; + backdrop-filter: blur(12px); + border-radius: 40px; + display: flex; + flex-direction: column; + justify-content: center; + padding: 48px 32px; + width: 100%; +} + +.approve-content { + margin: 16px 0; +} + +.approve-title { + font-size: 20px; + font-style: normal; + font-weight: 600; + line-height: 130%; /* 31.2px */ +} + +.approve-description { + font-variant-numeric: lining-nums tabular-nums; + font-size: 14px; + font-weight: 400; + line-height: 130%; /* 20.8px */ +} + +.message-flow-title { + align-items: center; + display: flex; + gap: 6px; + margin-bottom: 24px; +} + +.message-flow-title-text { + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 100%; /* 16px */ +} + +.message-input-container { + width: 100%; +} + +.message-input { + background: transparent; + border: none; + font-family: 'Inter', sans-serif; + font-size: 24px; + font-style: normal; + font-variant-numeric: lining-nums tabular-nums; + font-weight: 400; + line-height: 110%; + outline: none; + resize: none; + text-align: left; + width: 100%; + min-height: 32px; + max-height: 160px; + box-sizing: border-box; +} + +.message-input-footer { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.message-input-length-container { + align-items: flex-start; + display: flex; + flex-direction: column; + gap: 4px; +} + +.message-input-length-container-inner { + align-items: center; + display: flex; + gap: 4px; +} + +.message-input-length-max { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 100%; /* 14px */ +} + +.message-input-length-characters { + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 100%; /* 12px */ +} + +.message-input-length { + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 100%; /* 24px */ +} + +.message-separator { + height: 1px; + margin: 24px 0; + width: 100%; +} + +.message-unsupported-network { + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 100%; /* 14px */ + text-align: right; + margin-bottom: 12px; + width: 100%; +} + +[data-theme='light'] .message-input { + color: var(--primary-dark); +} + +[data-theme='dark'] .message-input { + color: var(--primary-light); +} + +[data-theme='light'] .message-input-length-max, +[data-theme='dark'] .message-unsupported-network, +[data-theme='dark'] .approve-description, +[data-theme='dark'] .message-input-length-characters { + color: #a9acb0; +} + +[data-theme='dark'] .message-input-length-max, +[data-theme='light'] .message-unsupported-network, +[data-theme='light'] .approve-description, +[data-theme='light'] .message-input-length-characters { + color: #696e75; +} + +[data-theme='light'] .message-separator { + background: #e5e8ec; +} + +[data-theme='dark'] .message-separator { + background: #283442; +} + +[data-theme='light'] .message-flow-container, +[data-theme='light'] .approve-container { + background: #ffffffcc; + border: 1px solid #e5e8ec; +} + +[data-theme='dark'] .message-flow-container, +[data-theme='dark'] .approve-container { + background: #171f29cc; + border: 1px solid #283442; +} + +@media (min-width: 768px) { + .approve-title { + font-size: 24px; + } + + .approve-description { + font-size: 16px; + } + + .message-input-length-container { + align-items: center; + flex-direction: row; + gap: 16px; + } + + .message-input-length { + font-size: 24px; + } + + .message-input-length-max { + font-size: 14px; + } + + .message-input-length-characters { + font-size: 14px; + } + + .message-separator { + margin: 32px 0; + } +} + +@media (min-width: 1168px) { + .message-flow-container, + .approve-container { + flex: 1; + width: 568px; + } +} diff --git a/examples/hello/frontend/src/MessageFlowCard.tsx b/examples/hello/frontend/src/MessageFlowCard.tsx new file mode 100644 index 00000000..c161e5a7 --- /dev/null +++ b/examples/hello/frontend/src/MessageFlowCard.tsx @@ -0,0 +1,178 @@ +import './MessageFlowCard.css'; + +import { evmCall } from '@zetachain/toolkit/chains/evm'; +import { ethers, ZeroAddress } from 'ethers'; +import { useEffect, useRef, useState } from 'react'; + +import { Button } from './components/Button'; +import { IconApprove, IconEnvelope, IconSendTitle } from './components/icons'; +import { ConfirmedContent } from './ConfirmedContent'; +import type { SupportedChain } from './constants/chains'; +import type { EIP6963ProviderDetail } from './types/wallet'; +import { formatNumberWithLocale } from './utils/formatNumber'; + +interface MessageFlowCardProps { + selectedProvider: EIP6963ProviderDetail; + supportedChain: SupportedChain | undefined; +} + +export function MessageFlowCard({ + selectedProvider, + supportedChain, +}: MessageFlowCardProps) { + const MAX_STRING_LENGTH = 2000; + const [isUserSigningTx, setIsUserSigningTx] = useState(false); + const [isTxReceiptLoading, setIsTxReceiptLoading] = useState(false); + const [stringValue, setStringValue] = useState(''); + const [connectedChainTxHash, setConnectedChainTxHash] = useState(''); + const textareaRef = useRef(null); + + const getStringByteLength = (string: string) => { + return new TextEncoder().encode(string).length; + }; + + const handleEvmCall = async () => { + try { + const ethersProvider = new ethers.BrowserProvider( + selectedProvider.provider + ); + const signer = + (await ethersProvider.getSigner()) as ethers.AbstractSigner; + + const helloUniversalContractAddress = + '0x61a184EB30D29eD0395d1ADF38CC7d2F966c4A82'; + + const evmCallParams = { + receiver: helloUniversalContractAddress, + types: ['string'], + values: [stringValue], + revertOptions: { + callOnRevert: false, + revertAddress: ZeroAddress, + revertMessage: '', + abortAddress: ZeroAddress, + onRevertGasLimit: 1000000, + }, + }; + + const evmCallOptions = { + signer, + txOptions: { + gasLimit: 1000000, + }, + }; + + setIsUserSigningTx(true); + + const result = await evmCall(evmCallParams, evmCallOptions); + + setIsTxReceiptLoading(true); + + await result.wait(); + + setConnectedChainTxHash(result.hash); + } catch (error) { + console.error(error); + } finally { + setIsUserSigningTx(false); + setIsTxReceiptLoading(false); + } + }; + + // Auto-resize textarea based on content + useEffect(() => { + const textarea = textareaRef.current; + if (textarea) { + // Reset height to auto to get the correct scrollHeight + textarea.style.height = 'auto'; + // Set height to scrollHeight to fit content + textarea.style.height = `${textarea.scrollHeight}px`; + } + }, [stringValue]); + + if (connectedChainTxHash || isTxReceiptLoading) { + return ( + { + setConnectedChainTxHash(''); + setStringValue(''); + }} + /> + ); + } + + if (isUserSigningTx) { + return ( +
+ +
+

Approve from Wallet

+

+ Awaiting approval via your wallet +

+
+
+ ); + } + + return ( +
+
+ + Message to Send +
+
+