Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
eba6cd5d68 | |||
|
5baccf3a84 | ||
b39afe386f | |||
565d1887e0 | |||
|
e6fa6aabd7 | ||
3fe0576f34 | |||
|
31e3b75dfa | ||
bee7379e86 | |||
bff5ab9f31 | |||
051de43480 | |||
ad614aff2f | |||
c038085b87 | |||
09104b50bf | |||
663eb42a74 | |||
8ba837b2f4 | |||
fc1c8df06b | |||
aa9aed89f2 | |||
74542a56a5 | |||
989251dc58 | |||
c041f031dc | |||
11f872032e |
@ -3,7 +3,8 @@ REACT_APP_ETHEREUM_MAINNET_CHAIN_ID=1
|
||||
REACT_APP_LACONICD_CHAIN_ID=laconic_9000-1
|
||||
REACT_APP_REGISTRY_GQL_ENDPOINT=http://localhost:9473/api
|
||||
REACT_APP_LACONICD_RPC_ENDPOINT=http://localhost:26657
|
||||
REACT_APP_LACONICD_DENOM=alnt
|
||||
REACT_APP_FAUCET_ENDPOINT=http://localhost:4000
|
||||
REACT_APP_WALLET_META_URL=http://localhost:3000
|
||||
REACT_APP_SUMSUB_API_ENDPOINT=
|
||||
REACT_APP_STAKING_AMOUNT=1000000000000000
|
||||
REACT_APP_LACONICD_DENOM=alnt
|
||||
|
@ -12,6 +12,7 @@
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }]
|
||||
"indent": ["error", 2, { "SwitchCase": 1 }],
|
||||
"semi": ["error", "always"]
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
trailingComma: 'all',
|
||||
printWidth: 100,
|
||||
tabWidth: 2,
|
||||
arrowParens: 'always',
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "testnet-onboarding-app",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.5",
|
||||
"@cosmjs/encoding": "^0.32.4",
|
||||
"@cosmjs/proto-signing": "^0.32.4",
|
||||
"@cosmjs/stargate": "^0.32.4",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
@ -23,16 +25,17 @@
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"ethers": "5.7.2",
|
||||
"https-browserify": "^1.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"notistack": "^3.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-pdf": "^9.1.0",
|
||||
"react-refresh": "^0.11.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"resolve": "^1.20.0",
|
||||
"semver": "^7.3.5",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"tailwind-variants": "^0.2.1",
|
||||
"tailwindcss": "^3.4.8",
|
||||
"tailwindcss": "^3.0.2",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"scripts": {
|
||||
|
BIN
public/TermsAndConditions.pdf
Normal file
BIN
public/TermsAndConditions.pdf
Normal file
Binary file not shown.
@ -1,10 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" viewBox="0 0 116 20" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.41155 10.5194C5.76482 8.19185 7.22144 4.97748 7.22079 1.42853C7.22166 0.94681 7.19477 0.470456 7.14132 0L0 0.000643078L0.000216722 13.5723C-0.0004338 15.2174 0.633716 16.863 1.90213 18.1175C3.17064 19.3721 4.83555 20.0001 6.49904 19.9993L6.49861 19.9997L20.2204 20L20.2199 12.9355C19.7453 12.8838 19.2637 12.857 18.7756 12.8569C15.1884 12.8574 11.9385 14.298 9.58522 16.6255C7.87283 18.2768 5.12731 18.2771 3.43606 16.6043C1.74589 14.9325 1.74513 12.2161 3.41155 10.5194ZM18.7392 1.46863C16.767 -0.481929 13.5629 -0.48268 11.5901 1.46863C9.61732 3.41984 9.61808 6.58895 11.5901 8.53941C13.5633 10.491 16.7663 10.4907 18.7392 8.53941C20.712 6.5882 20.7123 3.42016 18.7392 1.46863Z" fill="#0F0F0F"/>
|
||||
<path d="M31.8223 18.5836H39.6891V16.33H34.4518V1.41333H31.8223V18.5836Z" fill="#0F0F0F"/>
|
||||
<path d="M50.3602 1.41333H45.9994L41.4414 18.5836H44.1586L45.2981 14.2911H50.9299L52.0694 18.5836H54.9182L50.3602 1.41333ZM45.846 12.1448L48.125 3.25912H48.2126L50.404 12.1448H45.846Z" fill="#0F0F0F"/>
|
||||
<path d="M63.6228 8.06818H66.6907C66.6907 3.17467 65.091 1.07129 61.3657 1.07129C57.4432 1.07129 55.7559 3.73274 55.7559 9.97842C55.7559 16.2456 57.4432 18.9284 61.3657 18.9284C65.091 18.9284 66.6907 16.8894 66.7126 12.1461H63.6447C63.6228 15.8593 63.1626 16.7822 61.3657 16.7822C59.3059 16.7822 58.8018 15.43 58.8237 9.97842C58.8237 4.54829 59.3278 3.19611 61.3657 3.21756C63.1626 3.21756 63.6228 4.18346 63.6228 8.06818Z" fill="#0F0F0F"/>
|
||||
<path d="M74.5924 1.07142C78.5807 1.09297 80.2899 3.77576 80.2899 10C80.2899 16.2242 78.5807 18.9071 74.5924 18.9286C70.5823 18.95 68.873 16.2671 68.873 10C68.873 3.73286 70.5823 1.04997 74.5924 1.07142ZM71.9409 10C71.9409 15.4301 72.4669 16.7823 74.5924 16.7823C76.6961 16.7823 77.2221 15.4301 77.2221 10C77.2221 4.54841 76.6961 3.19624 74.5924 3.2178C72.4669 3.23924 71.9409 4.59142 71.9409 10Z" fill="#0F0F0F"/>
|
||||
<path d="M86.0202 18.5622L83.3906 18.5836V1.41333H88.0144L92.3314 15.4071H92.3752V1.41333H95.0043V18.5836H90.666L86.0642 3.51671H86.0202V18.5622Z" fill="#0F0F0F"/>
|
||||
<path d="M101.576 1.41333H98.9473V18.5836H101.576V1.41333Z" fill="#0F0F0F"/>
|
||||
<path d="M112.365 8.06818H115.433C115.433 3.17467 113.833 1.07129 110.108 1.07129C106.186 1.07129 104.498 3.73274 104.498 9.97842C104.498 16.2456 106.186 18.9284 110.108 18.9284C113.833 18.9284 115.433 16.8894 115.455 12.1461H112.387C112.365 15.8593 111.905 16.7822 110.108 16.7822C108.048 16.7822 107.545 15.43 107.566 9.97842C107.566 4.54829 108.07 3.19611 110.108 3.21756C111.905 3.21756 112.365 4.18346 112.365 8.06818Z" fill="#0F0F0F"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.6 KiB |
@ -1,5 +0,0 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Logo">
|
||||
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M2.6996 8.91554C4.56166 7.05349 5.71417 4.482 5.71371 1.6428C5.71446 1.25743 5.69309 0.876343 5.6508 0.5L0 0.500514L0.000228446 11.3578C-0.00028584 12.6739 0.501486 13.9904 1.5052 14.9941C2.50886 15.9977 3.82634 16.5001 5.14257 16.4994L5.14229 16.4997L16 16.5L15.9997 10.8485C15.6241 10.807 15.243 10.7857 14.8568 10.7856C12.0183 10.7859 9.44674 11.9385 7.58469 13.8005C6.22971 15.1214 4.0572 15.1217 2.71897 13.7835C1.38149 12.4461 1.38097 10.2729 2.6996 8.91554ZM14.8279 1.67491C13.2675 0.114457 10.7321 0.113886 9.17109 1.67491C7.61006 3.23589 7.61063 5.77114 9.17109 7.33154C10.7324 8.8928 13.2669 8.89257 14.8279 7.33154C16.389 5.77057 16.3893 3.23611 14.8279 1.67491Z" fill="#0F0F0F"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 875 B |
@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
@ -12,6 +13,11 @@
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
|
||||
rel="stylesheet" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
@ -21,24 +27,52 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
crossorigin="anonymous"
|
||||
rel="preload"
|
||||
href="https://laconic.com/fonts/tt-hoves/TTHoves-Regular.woff2"
|
||||
as="font"
|
||||
/>
|
||||
<title>Testnet Onboarding App</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loader-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 16px solid #e3e3e3;
|
||||
border-top: 16px solid #1976d2;
|
||||
border-radius: 50%;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="flex flex-col h-screen"></div>
|
||||
<div id="root">
|
||||
<div class="loader-wrapper">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
@ -50,4 +84,5 @@
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
21
public/pdf.worker.min.mjs
Normal file
21
public/pdf.worker.min.mjs
Normal file
File diff suppressed because one or more lines are too long
38
src/App.css
Normal file
38
src/App.css
Normal file
@ -0,0 +1,38 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
105
src/App.tsx
105
src/App.tsx
@ -8,30 +8,125 @@ import PageNotFound from "./pages/PageNotFound";
|
||||
import OnboardingSuccess from "./pages/OnboardingSuccess";
|
||||
import SignPageLayout from "./layout/SignPageLayout";
|
||||
import UserVerification from "./pages/UserVerification";
|
||||
import TermsAndConditions from "./pages/TermsAndConditions";
|
||||
import LandingPage from "./pages/LandingPage";
|
||||
import Header from "./components/Header";
|
||||
import { WalletConnectProvider } from "./context/WalletConnectContext";
|
||||
import VerifyEmail from "./pages/VerifyEmail";
|
||||
import Email from "./pages/Email";
|
||||
import Thanks from "./pages/Thanks";
|
||||
import Validator from "./pages/Validator";
|
||||
import ValidatorSuccess from "./pages/ValidatorSuccess";
|
||||
import { createTheme, Box, ThemeProvider, CssBaseline } from "@mui/material";
|
||||
|
||||
const darkTheme = createTheme({
|
||||
components: {
|
||||
MuiAccordion: {
|
||||
defaultProps: {
|
||||
sx: {
|
||||
border: "1px solid #48474F",
|
||||
borderBottomRightRadius: 3,
|
||||
borderBottomLeftRadius: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
defaultProps: {
|
||||
color: "primary",
|
||||
sx: {
|
||||
fontFamily: `DM Mono, monospace`,
|
||||
fontWeight: 400,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiLink: {
|
||||
defaultProps: {
|
||||
color: "text.primary",
|
||||
fontSize: "14px",
|
||||
},
|
||||
},
|
||||
MuiTypography: {
|
||||
defaultProps: {
|
||||
color: "text.primary",
|
||||
fontWeight: 400,
|
||||
},
|
||||
},
|
||||
MuiPaper: {
|
||||
defaultProps: {
|
||||
sx: {
|
||||
backgroundImage: "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
mode: "dark",
|
||||
primary: {
|
||||
main: "#0000F4",
|
||||
},
|
||||
secondary: {
|
||||
main: "#A2A2FF",
|
||||
},
|
||||
error: {
|
||||
main: "#B20710",
|
||||
},
|
||||
background: {
|
||||
default: "#0F0F0F",
|
||||
paper: "#18181A",
|
||||
},
|
||||
text: {
|
||||
primary: "#FBFBFB",
|
||||
},
|
||||
info: {
|
||||
main: "#FBFBFB",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100vh",
|
||||
width: "100vw",
|
||||
backgroundColor: "background.default",
|
||||
}}
|
||||
>
|
||||
<Header />
|
||||
<CssBaseline />
|
||||
<WalletConnectProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<TermsAndConditions />} />
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/verify-email" element={<VerifyEmail />} />
|
||||
<Route path="/email" element={<Email />} />
|
||||
<Route path="/connect-wallet" element={<ConnectWallet />} />
|
||||
<Route path="/thanks" element={<Thanks />} />
|
||||
<Route element={<SignPageLayout />}>
|
||||
<Route path="/sign-with-nitro-key" element={<SignWithNitroKey />} />
|
||||
<Route path="/user-verification" element={<UserVerification />} />
|
||||
<Route
|
||||
path="/sign-with-nitro-key"
|
||||
element={<SignWithNitroKey />}
|
||||
/>
|
||||
<Route
|
||||
path="/user-verification"
|
||||
element={<UserVerification />}
|
||||
/>
|
||||
<Route path="/sign-with-cosmos" element={<SignWithCosmos />} />
|
||||
<Route
|
||||
path="/onboarding-success"
|
||||
element={<OnboardingSuccess />}
|
||||
></Route>
|
||||
/>
|
||||
<Route path="/validator" element={<Validator />} />
|
||||
<Route
|
||||
path="/validator-success"
|
||||
element={<ValidatorSuccess />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
</WalletConnectProvider>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
25
src/components/CodeBlock.tsx
Normal file
25
src/components/CodeBlock.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Box } from "@mui/material";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
export const CodeBlock: React.FC<PropsWithChildren> = ({ children }) => (
|
||||
<Box
|
||||
sx={{
|
||||
backgroundColor: "#48474F",
|
||||
padding: 3,
|
||||
wordWrap: "break-word",
|
||||
mt: 1,
|
||||
borderRadius: 1,
|
||||
}}
|
||||
>
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
margin: 0,
|
||||
backgroundColor: "#48474F",
|
||||
color: "#FBFBFB",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
</Box>
|
||||
);
|
21
src/components/Container.tsx
Normal file
21
src/components/Container.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Box, BoxProps } from "@mui/material";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
|
||||
export const Container: React.FC<
|
||||
PropsWithChildren<{ boxProps?: BoxProps }>
|
||||
> = ({ children, boxProps = {} }) => (
|
||||
<Box
|
||||
{...boxProps}
|
||||
sx={{
|
||||
width: "100%",
|
||||
maxWidth: "752px",
|
||||
mx: "auto",
|
||||
backgroundColor: "background.paper",
|
||||
padding: 3,
|
||||
borderRadius: 2,
|
||||
...boxProps.sx,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
@ -1,25 +1,91 @@
|
||||
import React from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
|
||||
import { BodyText } from "./ui/BodyText";
|
||||
import { AppBar, SvgIcon, Stack, Divider, Typography } from "@mui/material";
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="px-6 py-2.5 bg-neutral-10">
|
||||
<AppBar
|
||||
position="static"
|
||||
color="inherit"
|
||||
sx={{ boxShadow: "none", mb: 4, height: 48 }}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
sx={{
|
||||
backgroundColor: "background.paper",
|
||||
pl: 2,
|
||||
alignItems: "center",
|
||||
py: 1,
|
||||
}}
|
||||
spacing={1}
|
||||
>
|
||||
<Link
|
||||
to={location.pathname === "/" ? "/" : "/connect-wallet"}
|
||||
style={{ color: "inherit", textDecoration: "none" }}
|
||||
style={{
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<img alt="Laconic logomark" className="h-5 md:hidden" src="/img/logomark.svg" />
|
||||
<img alt="Laconic logo" className="h-5 hidden md:block" src="/img/logo.svg" />
|
||||
<div className="border-l border-black h-5"></div>
|
||||
<BodyText>Testnet Onboarding</BodyText>
|
||||
</div>
|
||||
<SvgIcon sx={{ height: 20, width: 100 }}>
|
||||
<svg
|
||||
width="115"
|
||||
height="20"
|
||||
viewBox="0 0 115 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M3.37388 10.5194C5.70149 8.19185 7.14225 4.97748 7.1416 1.42853C7.14246 0.94681 7.11586 0.470456 7.063 0L-0.000488281 0.000643078L-0.000273922 13.5723C-0.000917354 15.2174 0.62632 16.863 1.88091 18.1175C3.1356 19.3721 4.78235 20.0001 6.42772 19.9993L6.42729 19.9997L19.9995 20L19.999 12.9355C19.5296 12.8838 19.0532 12.857 18.5704 12.8569C15.0224 12.8574 11.8079 14.298 9.48026 16.6255C7.78654 18.2768 5.07093 18.2771 3.39812 16.6043C1.72638 14.9325 1.72562 12.2161 3.37388 10.5194ZM18.5344 1.46863C16.5837 -0.481929 13.4146 -0.48268 11.4633 1.46863C9.512 3.41984 9.51276 6.58895 11.4633 8.53941C13.415 10.491 16.5831 10.4907 18.5344 8.53941C20.4857 6.5882 20.4861 3.42016 18.5344 1.46863Z"
|
||||
fill="#FBFBFB"
|
||||
/>
|
||||
<path
|
||||
d="M31.4741 18.5838H39.2552V16.3302H34.075V1.41351H31.4741V18.5838Z"
|
||||
fill="#FBFBFB"
|
||||
/>
|
||||
<path
|
||||
d="M49.8108 1.41351H45.4976L40.9893 18.5838H43.6769L44.8039 14.2913H50.3744L51.5014 18.5838H54.3191L49.8108 1.41351ZM45.3458 12.145L47.6 3.2593H47.6866L49.8541 12.145H45.3458Z"
|
||||
fill="#FBFBFB"
|
||||
/>
|
||||
<path
|
||||
d="M62.9292 8.06885H65.9636C65.9636 3.17534 64.3813 1.07196 60.6967 1.07196C56.8169 1.07196 55.1479 3.73341 55.1479 9.97909C55.1479 16.2462 56.8169 18.9291 60.6967 18.9291C64.3813 18.9291 65.9636 16.8901 65.9853 12.1468H62.9508C62.9292 15.8599 62.474 16.7828 60.6967 16.7828C58.6593 16.7828 58.1607 15.4307 58.1824 9.97909C58.1824 4.54896 58.6809 3.19678 60.6967 3.21823C62.474 3.21823 62.9292 4.18413 62.9292 8.06885Z"
|
||||
fill="#FBFBFB"
|
||||
/>
|
||||
<path
|
||||
d="M73.7781 1.07209C77.7229 1.09364 79.4135 3.77643 79.4135 10.0007C79.4135 16.2249 77.7229 18.9078 73.7781 18.9292C69.8117 18.9507 68.1211 16.2678 68.1211 10.0007C68.1211 3.73354 69.8117 1.05064 73.7781 1.07209ZM71.1555 10.0007C71.1555 15.4308 71.6757 16.783 73.7781 16.783C75.8589 16.783 76.3791 15.4308 76.3791 10.0007C76.3791 4.54909 75.8589 3.19691 73.7781 3.21847C71.6757 3.23992 71.1555 4.59209 71.1555 10.0007Z"
|
||||
fill="#FBFBFB"
|
||||
/>
|
||||
<path
|
||||
d="M85.0819 18.5624L82.481 18.5838V1.41351H87.0544L91.3243 15.4073H91.3676V1.41351H93.968V18.5838H89.677L85.1254 3.51689H85.0819V18.5624Z"
|
||||
fill="#FBFBFB"
|
||||
/>
|
||||
<path
|
||||
d="M100.468 1.41351H97.8677V18.5838H100.468V1.41351Z"
|
||||
fill="#FBFBFB"
|
||||
/>
|
||||
<path
|
||||
d="M111.139 8.06885H114.174C114.174 3.17534 112.591 1.07196 108.906 1.07196C105.028 1.07196 103.358 3.73341 103.358 9.97909C103.358 16.2462 105.028 18.9291 108.906 18.9291C112.591 18.9291 114.174 16.8901 114.195 12.1468H111.161C111.139 15.8599 110.684 16.7828 108.906 16.7828C106.869 16.7828 106.371 15.4307 106.393 9.97909C106.393 4.54896 106.891 3.19678 108.906 3.21823C110.684 3.21823 111.139 4.18413 111.139 8.06885Z"
|
||||
fill="#FBFBFB"
|
||||
/>
|
||||
</svg>
|
||||
</SvgIcon>
|
||||
</Link>
|
||||
</div>
|
||||
<Divider
|
||||
flexItem
|
||||
orientation="vertical"
|
||||
color="#FBFBFB"
|
||||
sx={{ height: "1.2rem", alignSelf: "center", width: "1px" }}
|
||||
/>
|
||||
<Typography fontSize="1.25rem">Testnet Onboarding</Typography>
|
||||
</Stack>
|
||||
</AppBar>
|
||||
);
|
||||
};
|
||||
|
||||
|
83
src/components/SelectRoleCard.tsx
Normal file
83
src/components/SelectRoleCard.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { Typography, Button, Box, Paper, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel, Link, Checkbox } from '@mui/material';
|
||||
|
||||
import TermsAndConditionsDialog from './TermsAndConditionsDialog';
|
||||
|
||||
export enum Role {
|
||||
Validator = 'validator',
|
||||
Participant = 'participant'
|
||||
}
|
||||
|
||||
const SelectRoleCard = ({ handleAccept, handleRoleChange }: { handleAccept: () => void, handleRoleChange: (role: Role) => void }) => {
|
||||
const [selectedRole, setSelectedRole] = useState<Role>(Role.Participant);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [isHidden, setIsHidden] = useState(false);
|
||||
|
||||
const [isDialogOpen, setisDialogOpen] = useState(false);
|
||||
|
||||
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setChecked(event.target.checked);
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
handleAccept();
|
||||
setIsHidden(true);
|
||||
};
|
||||
|
||||
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedRole(event.target.value as Role);
|
||||
handleRoleChange((event.target as HTMLInputElement).value === Role.Validator ? Role.Validator : Role.Participant);
|
||||
setChecked(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper elevation={3} sx={{ padding: 2, marginTop: 2, display: isHidden ? "none" : "block", marginBottom: 3 }} >
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Select your role</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="roles"
|
||||
name="roles"
|
||||
value={selectedRole}
|
||||
onChange={handleRadioChange}
|
||||
>
|
||||
<FormControlLabel
|
||||
value={Role.Validator}
|
||||
control={<Radio />}
|
||||
label="Validator / Service Provider"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value={Role.Participant}
|
||||
control={<Radio />}
|
||||
label="App Publisher"
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<Box mt={2} display="flex" alignItems="center">
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={handleCheckboxChange}
|
||||
color="primary"
|
||||
/>
|
||||
<Typography variant="body1">
|
||||
I accept the <Link onClick={() => setisDialogOpen(true)} target="_blank" rel="noopener">
|
||||
terms and conditions
|
||||
</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box mt={4} display="flex" justifyContent="end">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleContinue}
|
||||
disabled={!checked}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</Box>
|
||||
<TermsAndConditionsDialog open={isDialogOpen} onClose={() => setisDialogOpen(false)}/>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectRoleCard;
|
65
src/components/TermsAndConditionsBox.tsx
Normal file
65
src/components/TermsAndConditionsBox.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React, { useState } from "react";
|
||||
import { Document, Page, pdfjs } from "react-pdf";
|
||||
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
// https://github.com/wojtekmaj/react-pdf?tab=readme-ov-file#copy-worker-to-public-directory
|
||||
pdfjs.GlobalWorkerOptions.workerSrc =
|
||||
process.env.PUBLIC_URL + "/pdf.worker.min.mjs";
|
||||
|
||||
interface TermsAndConditionsBoxProps {
|
||||
height: string;
|
||||
onLoad?: () => void;
|
||||
}
|
||||
|
||||
const TermsAndConditionsBox = ({
|
||||
height,
|
||||
onLoad,
|
||||
}: TermsAndConditionsBoxProps) => {
|
||||
const [numPages, setNumPages] = useState<number>();
|
||||
|
||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
||||
setNumPages(numPages);
|
||||
|
||||
if (onLoad) {
|
||||
onLoad();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h4" textAlign="center" gutterBottom>
|
||||
Terms and Conditions
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
height: height,
|
||||
overflowY: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Document
|
||||
file={process.env.PUBLIC_URL + "/TermsAndConditions.pdf"}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
>
|
||||
{Array.apply(null, Array(numPages))
|
||||
.map((x, i) => i + 1)
|
||||
.map((page) => {
|
||||
return (
|
||||
<Page
|
||||
key={page}
|
||||
pageNumber={page}
|
||||
renderTextLayer={false}
|
||||
renderAnnotationLayer={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Document>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsAndConditionsBox;
|
@ -1,102 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import TermsAndConditionsDialog from "./TermsAndConditionsDialog";
|
||||
import { Card } from "./ui/Card";
|
||||
import { Button } from "./ui/Button";
|
||||
|
||||
export enum Role {
|
||||
Validator = "validator",
|
||||
Participant = "participant",
|
||||
}
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
handleAccept: () => void;
|
||||
handleRoleChange: (role: Role) => void;
|
||||
};
|
||||
|
||||
const TermsAndConditionsCard = ({ className, handleAccept, handleRoleChange }: Props) => {
|
||||
const [selectedRole, setSelectedRole] = useState<Role>(Role.Participant);
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
const [isDialogOpen, setisDialogOpen] = useState(false);
|
||||
|
||||
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setChecked(event.target.checked);
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
handleAccept();
|
||||
};
|
||||
|
||||
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSelectedRole(event.target.value as Role);
|
||||
handleRoleChange(
|
||||
(event.target as HTMLInputElement).value === Role.Validator
|
||||
? Role.Validator
|
||||
: Role.Participant,
|
||||
);
|
||||
setChecked(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={`body-2 ${className || ""}`}>
|
||||
<div>
|
||||
<label className="block text-sm">Select your role</label>
|
||||
<fieldset className="mt-2 flex flex-col">
|
||||
<label className="inline-flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
className="h-4 w-4 border-gray-300 text-primary-60 focus:primary-30"
|
||||
value={Role.Validator}
|
||||
checked={selectedRole === Role.Validator}
|
||||
onChange={handleRadioChange}
|
||||
/>
|
||||
<span className="ml-2">Validator</span>
|
||||
</label>
|
||||
<label className="inline-flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
className="h-4 w-4 border-gray-300 text-primary-60 focus:primary-30"
|
||||
value={Role.Participant}
|
||||
checked={selectedRole === Role.Participant}
|
||||
onChange={handleRadioChange}
|
||||
/>
|
||||
<span className="ml-2">Participant</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
</div>
|
||||
<fieldset className="flex items-center">
|
||||
<input
|
||||
id="toc-check"
|
||||
type="checkbox"
|
||||
className="form-checkbox"
|
||||
checked={checked}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label htmlFor="toc-check" className="ml-2">
|
||||
I accept the{" "}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() => {
|
||||
setisDialogOpen(true);
|
||||
}}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Terms & Conditions
|
||||
</button>
|
||||
</label>
|
||||
</fieldset>
|
||||
<Button size="lg" className={`py-2 md:self-end`} onClick={handleContinue} disabled={!checked}>
|
||||
Continue
|
||||
</Button>
|
||||
<TermsAndConditionsDialog
|
||||
isValidator={selectedRole === Role.Validator}
|
||||
open={isDialogOpen}
|
||||
onClose={() => setisDialogOpen(false)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsAndConditionsCard;
|
@ -1,41 +1,27 @@
|
||||
import React from "react";
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
} from "@mui/material";
|
||||
import { Button, Dialog, DialogActions, DialogContent } from '@mui/material';
|
||||
|
||||
import { TNC_PARTICIPANT_CONTENT, TNC_VALIDATOR_CONTENT } from "../constants";
|
||||
import { Button } from "./ui/Button";
|
||||
import TermsAndConditionsBox from './TermsAndConditionsBox';
|
||||
|
||||
interface TermsDialogProps {
|
||||
isValidator: boolean;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const TermsAndConditionsDialog: React.FC<TermsDialogProps> = ({ isValidator, open, onClose }) => {
|
||||
const TermsAndConditionsDialog: React.FC<TermsDialogProps> = ({ open, onClose }) => {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>
|
||||
<h3>Terms and Conditions</h3>
|
||||
</DialogTitle>
|
||||
<Dialog open={open} onClose={onClose} maxWidth="lg">
|
||||
<DialogContent>
|
||||
<h5 className="mb-4">Onboard as a {isValidator ? "validator" : "participant"}</h5>
|
||||
|
||||
<DialogContentText>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: isValidator ? TNC_VALIDATOR_CONTENT : TNC_PARTICIPANT_CONTENT,
|
||||
}}
|
||||
/>
|
||||
</DialogContentText>
|
||||
<TermsAndConditionsBox height='65vh' />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} className="px-3 py-1">
|
||||
<Button>
|
||||
<a href="/TermsAndConditions.pdf" download style={{textDecoration: "none", color: "inherit"}}>
|
||||
Download PDF
|
||||
</a>
|
||||
</Button>
|
||||
<Button onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
@ -1,10 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
sizing?: 1 | 2 | 3;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const BodyText = ({ sizing = 1, className, children }: React.PropsWithChildren<Props>) => {
|
||||
return <div className={`body-${sizing} ${className}`}>{children}</div>;
|
||||
};
|
@ -1,100 +0,0 @@
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import { tv, VariantProps } from "tailwind-variants";
|
||||
|
||||
type Props = {
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
} & ButtonTheme &
|
||||
ComponentPropsWithoutRef<"button">;
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, Props>(
|
||||
({ loading, className, children, variant = "primary", size = "md", ...props }, ref) => {
|
||||
return (
|
||||
<button {...props} ref={ref} className={buttonTheme({ variant, size, className })}>
|
||||
<span className={loading ? "invisible" : ""}>{children}</span>
|
||||
{loading && (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-4 h-4 text-primary-40 animate-spin fill-primary-20 absolute top-1/2 left-1/2 -mt-2 -ml-1.5"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
/**
|
||||
* Defines the theme for a button component.
|
||||
*/
|
||||
export const buttonTheme = tv({
|
||||
base: [
|
||||
"relative",
|
||||
"h-fit",
|
||||
"inline-flex",
|
||||
"items-center",
|
||||
"justify-center",
|
||||
"whitespace-nowrap",
|
||||
"focus-ring",
|
||||
"disabled:cursor-not-allowed",
|
||||
"transition-colors",
|
||||
"duration-150",
|
||||
"font-mono",
|
||||
"rounded",
|
||||
"antialiased",
|
||||
"uppercase",
|
||||
],
|
||||
variants: {
|
||||
size: {
|
||||
lg: ["px-6", "py-2.5", "text-[18px]"],
|
||||
md: ["px-4", "py-2", "text-[16px]"],
|
||||
sm: ["px-3", "py-1", "text-[16px]"],
|
||||
},
|
||||
variant: {
|
||||
primary: [
|
||||
"text-cream",
|
||||
"bg-primary-60",
|
||||
"hover:bg-primary-50",
|
||||
"active:bg-primary-70",
|
||||
"disabled:bg-neutral-40",
|
||||
"disabled:text-neutral-70",
|
||||
],
|
||||
success: [
|
||||
"text-cream",
|
||||
"bg-success-40",
|
||||
"pointer-events-none", // Disable button; success state is not actionable
|
||||
],
|
||||
"danger-outline": [
|
||||
"text-danger-50",
|
||||
"border",
|
||||
"border-danger-50",
|
||||
"hover:border-danger-40",
|
||||
"hover:text-cream",
|
||||
"hover:bg-danger-40",
|
||||
"active:bg-danger-60",
|
||||
"active:border-danger-60",
|
||||
],
|
||||
unstyled: [],
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "md",
|
||||
variant: "primary",
|
||||
},
|
||||
});
|
||||
|
||||
type ButtonTheme = VariantProps<typeof buttonTheme>;
|
@ -1,46 +0,0 @@
|
||||
import React, { ComponentPropsWithoutRef, forwardRef } from "react";
|
||||
import { tv, VariantProps } from "tailwind-variants";
|
||||
|
||||
type Props = {} & Theme & ComponentPropsWithoutRef<"div">;
|
||||
|
||||
export const Card = forwardRef<HTMLButtonElement, Props>(
|
||||
({ children, className, type = "generic", ...props }) => {
|
||||
return (
|
||||
<div {...props} className={"Card " + theme({ type, className })}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Card.displayName = "Card";
|
||||
|
||||
type Theme = VariantProps<typeof theme>;
|
||||
export const theme = tv({
|
||||
base: [],
|
||||
variants: {
|
||||
type: {
|
||||
page: [
|
||||
"max-w-full",
|
||||
"md:max-w-[752px]",
|
||||
"mx-auto",
|
||||
"p-4",
|
||||
"md:p-8",
|
||||
"flex",
|
||||
"flex-col",
|
||||
"gap-4",
|
||||
"md:gap-8",
|
||||
],
|
||||
generic: [
|
||||
"px-4",
|
||||
"md:px-6",
|
||||
"py-6",
|
||||
"md:py-8",
|
||||
"flex",
|
||||
"flex-col",
|
||||
"gap-6",
|
||||
"md:gap-8",
|
||||
"bg-neutral-10",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
@ -1,33 +1,7 @@
|
||||
export const TNC_GENERIC_CONTENT = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
|
||||
export const WALLET_DISCLAIMER_MSG = 'You are connecting to an experimental wallet! It is not secure. Do not use it elsewhere and/or for managing real assets.';
|
||||
|
||||
export const TNC_VALIDATOR_CONTENT = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
|
||||
|
||||
export const TNC_PARTICIPANT_CONTENT = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. <br/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`;
|
||||
|
||||
export const WALLET_DISCLAIMER_MSG = 'You are connecting to an experimental wallet! It is not secure. Do not use it elsewhere and/or for managing real assets.'
|
||||
export const REDIRECT_EMAIL_MSG = 'Close this tab and the confirmation link in your email will bring you back to the onboarding app.';
|
||||
|
||||
export const ENABLE_KYC = false;
|
||||
|
||||
export const HASHED_SUBSCRIBER_ID_KEY = 'subscriberIdHash';
|
||||
|
@ -22,6 +22,7 @@ assert(PROJECT_ID, "Wallet connect project id not provided");
|
||||
interface ContextValue {
|
||||
connect: () => Promise<void>;
|
||||
session: SessionTypes.Struct | null;
|
||||
isSessionLoading: boolean;
|
||||
signClient: SignClient | undefined;
|
||||
checkPersistedState: (client: SignClient) => Promise<void>;
|
||||
disconnect: () => Promise<void>;
|
||||
@ -30,6 +31,7 @@ interface ContextValue {
|
||||
const walletConnectContext = createContext<ContextValue>({
|
||||
connect: () => Promise.resolve(),
|
||||
session: null,
|
||||
isSessionLoading: true,
|
||||
signClient: undefined,
|
||||
checkPersistedState: () => Promise.resolve(),
|
||||
disconnect: () => Promise.resolve(),
|
||||
@ -47,6 +49,7 @@ export const WalletConnectProvider = ({
|
||||
}) => {
|
||||
const [signClient, setSignClient] = useState<SignClient>();
|
||||
const [session, setSession] = useState<SessionTypes.Struct | null>(null);
|
||||
const [isSessionLoading, setIsSessionLoading] = useState(true);
|
||||
const isSignClientInitializing = useRef<boolean>(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -59,6 +62,7 @@ export const WalletConnectProvider = ({
|
||||
}
|
||||
|
||||
setSession(null);
|
||||
setIsSessionLoading(false);
|
||||
}, [signClient, session]);
|
||||
|
||||
const checkPersistedState = useCallback(async (client: SignClient) => {
|
||||
@ -67,6 +71,8 @@ export const WalletConnectProvider = ({
|
||||
const session = client.session.get(client.session.keys[lastKeyIndex]);
|
||||
setSession(session);
|
||||
}
|
||||
|
||||
setIsSessionLoading(false);
|
||||
}, []);
|
||||
|
||||
const subscribeToEvents = useCallback(
|
||||
@ -76,10 +82,12 @@ export const WalletConnectProvider = ({
|
||||
const currentSession = client.session.get(topic);
|
||||
const updatedSession = { ...currentSession, namespaces };
|
||||
setSession(updatedSession);
|
||||
setIsSessionLoading(false);
|
||||
});
|
||||
|
||||
client.on("session_delete", () => {
|
||||
setSession(null);
|
||||
setIsSessionLoading(false);
|
||||
navigate("/");
|
||||
});
|
||||
},
|
||||
@ -133,12 +141,14 @@ export const WalletConnectProvider = ({
|
||||
try {
|
||||
const session = await approval();
|
||||
setSession(session);
|
||||
setIsSessionLoading(false);
|
||||
} catch (error) {
|
||||
enqueueSnackbar("User rejected pairing request", { variant: "error" });
|
||||
console.log(error);
|
||||
}
|
||||
web3Modal.closeModal();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -152,6 +162,7 @@ export const WalletConnectProvider = ({
|
||||
value={{
|
||||
connect,
|
||||
session,
|
||||
isSessionLoading,
|
||||
signClient,
|
||||
checkPersistedState,
|
||||
disconnect,
|
||||
@ -163,12 +174,13 @@ export const WalletConnectProvider = ({
|
||||
};
|
||||
|
||||
export const useWalletConnectContext = () => {
|
||||
const { connect, session, signClient, checkPersistedState, disconnect } =
|
||||
const { connect, session, signClient, isSessionLoading, checkPersistedState, disconnect } =
|
||||
useContext(walletConnectContext);
|
||||
|
||||
return {
|
||||
connect,
|
||||
session,
|
||||
isSessionLoading,
|
||||
signClient,
|
||||
checkPersistedState,
|
||||
disconnect,
|
||||
|
@ -1,49 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
h1, h2, h3, h4 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
h5, h6 {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
h1 { font-size: 36px; @apply md:text-[48px]; }
|
||||
h2 { font-size: 32px; @apply md:text-[40px]; }
|
||||
h3 { font-size: 24px; @apply md:text-[32px]; }
|
||||
h4 { font-size: 20px; @apply md:text-[28px]; }
|
||||
h5 { font-size: 18px; @apply md:text-[24px]; }
|
||||
h6 { @apply md:text-[20px]; }
|
||||
|
||||
.body-1 { font-size: 16px; @apply md:text-[18px]; line-height: 1.5; }
|
||||
.body-2 { font-size: 14px; @apply md:text-[16px]; line-height: 1.5; }
|
||||
.body-3 { font-size: 14px; @apply md:text-[14px]; line-height: 1.5; }
|
||||
|
||||
.font-mono {
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
/* Global helpers */
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Fonts */
|
||||
@font-face {
|
||||
font-family: 'TTHoves';
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('https://laconic.com/fonts/tt-hoves/TTHoves-Regular.woff2') format('woff2');
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
34
src/layout/Layout.tsx
Normal file
34
src/layout/Layout.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Container } from "../components/Container";
|
||||
import { ArrowBack } from "@mui/icons-material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const Layout: React.FC<
|
||||
PropsWithChildren<{
|
||||
title: string;
|
||||
backLinkTitle?: string;
|
||||
noBackButton?: boolean;
|
||||
}>
|
||||
> = ({ children, title, backLinkTitle = "Home", noBackButton = false }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Container boxProps={{ sx: { backgroundColor: "inherit", padding: 0 } }}>
|
||||
{noBackButton ? null : (
|
||||
<Button
|
||||
startIcon={<ArrowBack />}
|
||||
color="info"
|
||||
sx={{ mb: 4 }}
|
||||
onClick={() => navigate("/")}
|
||||
>
|
||||
{backLinkTitle}
|
||||
</Button>
|
||||
)}
|
||||
<Typography variant="h4" sx={{ mb: 4 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Container>{children}</Container>
|
||||
</Container>
|
||||
);
|
||||
};
|
@ -1,48 +1,76 @@
|
||||
import React from "react";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import { Avatar, Button, Stack, Typography } from "@mui/material";
|
||||
|
||||
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
||||
import { Button } from "../components/ui/Button";
|
||||
import { Card } from "../components/ui/Card";
|
||||
import { Container } from "../components/Container";
|
||||
|
||||
const SignPageLayout = () => {
|
||||
const { disconnect, session } = useWalletConnectContext();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const disconnectHandler = async () => {
|
||||
const { pathname } = location;
|
||||
const redirectTo = pathname ? pathname.substring(1) : "";
|
||||
|
||||
await disconnect();
|
||||
navigate("/");
|
||||
navigate(`/connect-wallet?redirectTo=${redirectTo}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card type="page">
|
||||
<Stack justifyContent="center" alignItems="center">
|
||||
<Container
|
||||
boxProps={{
|
||||
sx: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
mb: 4,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{session && (
|
||||
<div className="p-4 flex flex-col md:flex-row md:items-center gap-4 bg-neutral-10 body-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-row items-end">
|
||||
<div>
|
||||
<span className="font-semibold">Connected to:</span> {session.peer.metadata.name}{" "}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-1 overflow-ellipsis overflow-hidden whitespace-nowrap">
|
||||
<span className="font-semibold">Session ID:</span> {session.topic}
|
||||
</div>
|
||||
<Stack spacing={0.5}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2">
|
||||
<b>Connected to:</b> {session.peer.metadata.name}
|
||||
</Typography>
|
||||
<Avatar
|
||||
variant="square"
|
||||
alt="Peer logo"
|
||||
src={session.peer.metadata.icons[0]}
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
marginLeft: 1,
|
||||
paddingBottom: 0.5,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Typography variant="body2">
|
||||
<b>Session ID:</b> {session.topic}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
<Button
|
||||
variant="danger-outline"
|
||||
size="sm"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={disconnectHandler}
|
||||
className="mr-auto"
|
||||
sx={{ color: "text.primary" }}
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
<Outlet />
|
||||
</Card>
|
||||
</>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,37 +1,61 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { Button, Box, Container } from "@mui/material";
|
||||
|
||||
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
||||
import { WALLET_DISCLAIMER_MSG } from "../constants";
|
||||
import { Button } from "../components/ui/Button";
|
||||
import { BodyText } from "../components/ui/BodyText";
|
||||
|
||||
const ConnectWallet = () => {
|
||||
const { connect, session } = useWalletConnectContext();
|
||||
const { connect, session, signClient, checkPersistedState } =
|
||||
useWalletConnectContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const redirectTo = searchParams.get("redirectTo");
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
navigate("/sign-with-nitro-key");
|
||||
if (signClient && !session) {
|
||||
checkPersistedState(signClient);
|
||||
}
|
||||
}, [session, navigate]);
|
||||
}, [checkPersistedState, signClient, session]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (redirectTo) {
|
||||
navigate(`/${redirectTo}`, {
|
||||
state: location.state,
|
||||
});
|
||||
} else {
|
||||
navigate("/sign-with-nitro-key", {
|
||||
state: location.state,
|
||||
});
|
||||
}
|
||||
}, [session, navigate, redirectTo, location.state]);
|
||||
|
||||
const handler = async () => {
|
||||
await connect();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-3 md:mt-8 max-w-md mx-auto">
|
||||
<div className="p-6 flex-center gap-4 bg-neutral-10">
|
||||
<h3 className="text-danger-50">Disclaimer</h3>
|
||||
<BodyText>{WALLET_DISCLAIMER_MSG}</BodyText>
|
||||
|
||||
<Button size="lg" onClick={handler} className="w-full md:w-auto">
|
||||
<Container maxWidth="lg" sx={{ height: "75%" }}>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
padding={5}
|
||||
justifyContent="center"
|
||||
height="100%"
|
||||
>
|
||||
<Button variant="contained" onClick={handler}>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
33
src/pages/Email.tsx
Normal file
33
src/pages/Email.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
import { REDIRECT_EMAIL_MSG } from '../constants';
|
||||
|
||||
const Email = () => {
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
marginY={20}
|
||||
marginX={50}
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: 'grey.500',
|
||||
}}
|
||||
padding={5}
|
||||
|
||||
>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
Thank you for registering!
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{REDIRECT_EMAIL_MSG}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Email;
|
86
src/pages/LandingPage.tsx
Normal file
86
src/pages/LandingPage.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button, Box, Typography } from "@mui/material";
|
||||
|
||||
import TermsAndConditionsBox from "../components/TermsAndConditionsBox";
|
||||
import { Container } from "../components/Container";
|
||||
|
||||
const LandingPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
|
||||
const handleAccept = () => {
|
||||
navigate("/verify-email");
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
margin={10}
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: "grey.500",
|
||||
borderRadius: 1,
|
||||
}}
|
||||
padding={5}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Welcome to the LORO Testnet Onboarding App. The detailed instructions
|
||||
for completing this first step are found in the{" "}
|
||||
<a
|
||||
href="https://github.com/hyphacoop/loro-testnet/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
LORO testnet repo
|
||||
</a>
|
||||
. Once your onboarding transaction has been submitted, await the
|
||||
completion of stage0. The genesis.json file and peer nodes will then
|
||||
be published in the aforementioned repository for validators to begin
|
||||
stage1. Once enough validators are online and the Laconic chain is
|
||||
running, those same validators can complete their service provider
|
||||
setup. Once service providers are live, app publishers can start
|
||||
deploying webapps to individual service providers.
|
||||
</Typography>
|
||||
</Box>
|
||||
<TermsAndConditionsBox
|
||||
height="43vh"
|
||||
onLoad={() => {
|
||||
setIsDisabled(false);
|
||||
}}
|
||||
/>
|
||||
<Box m={2} display="flex" justifyContent="center" gap={2}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
sx={{ color: "text.primary" }}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<a
|
||||
href="/TermsAndConditions.pdf"
|
||||
download
|
||||
style={{textDecoration: "none", color: "inherit"}}
|
||||
>
|
||||
Download PDF
|
||||
</a>
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleAccept}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default LandingPage;
|
@ -13,15 +13,9 @@ import {
|
||||
getAccessTokenExpirationHandler,
|
||||
options,
|
||||
} from "../utils/sumsub";
|
||||
import { ENABLE_KYC } from "../constants";
|
||||
import { Card } from "../components/ui/Card";
|
||||
|
||||
interface Participant {
|
||||
cosmosAddress: string;
|
||||
nitroAddress: string;
|
||||
role: string;
|
||||
kycId: string;
|
||||
}
|
||||
import { ENABLE_KYC, HASHED_SUBSCRIBER_ID_KEY } from "../constants";
|
||||
import { Participant } from "../types";
|
||||
import { CodeBlock } from "../components/CodeBlock";
|
||||
|
||||
const registry = new Registry(process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!);
|
||||
|
||||
@ -43,15 +37,20 @@ const OnboardingSuccess = () => {
|
||||
const fetchParticipants = async () => {
|
||||
try {
|
||||
if (!cosmosAddress) {
|
||||
enqueueSnackbar("Cosmos address is not provided", { variant: "error" });
|
||||
enqueueSnackbar("Laconic address is not provided", {
|
||||
variant: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const participant: Participant = await registry.getParticipantByAddress(cosmosAddress);
|
||||
const participant: Participant =
|
||||
await registry.getParticipantByAddress(cosmosAddress);
|
||||
if (!participant) {
|
||||
enqueueSnackbar("Participant not found", { variant: "error" });
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.removeItem(HASHED_SUBSCRIBER_ID_KEY);
|
||||
|
||||
setParticipant(participant);
|
||||
} catch (error) {
|
||||
console.error("Error fetching participants", error);
|
||||
@ -78,34 +77,38 @@ const OnboardingSuccess = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<section>
|
||||
<h4>Transaction Successful</h4>
|
||||
<Card className="mt-2">
|
||||
<div>
|
||||
<div className="body-2">Participant onboarded:</div>
|
||||
<div className="mt-2 p-4 break-words bg-cream">
|
||||
<pre style={{ whiteSpace: "pre-wrap" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: 6,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5">Transaction Successful</Typography>
|
||||
<Typography variant="body1">
|
||||
Participant onboarded: <br />
|
||||
</Typography>
|
||||
<CodeBlock>
|
||||
{participant && (
|
||||
<div>
|
||||
Cosmos Address: {participant.cosmosAddress} <br />
|
||||
Laconic Address: {participant.cosmosAddress} <br />
|
||||
Nitro Address: {participant.nitroAddress} <br />
|
||||
Role: {participant.role} <br />
|
||||
KYC ID: {participant.kycId} <br />
|
||||
<br />
|
||||
</div>
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
</CodeBlock>
|
||||
{ENABLE_KYC ? (
|
||||
<Box>
|
||||
<Typography variant="h5">KYC Status</Typography>
|
||||
{!loading && token && cosmosAddress && (
|
||||
<SumsubWebSdk
|
||||
accessToken={token}
|
||||
expirationHandler={getAccessTokenExpirationHandler(cosmosAddress)}
|
||||
expirationHandler={getAccessTokenExpirationHandler(
|
||||
cosmosAddress,
|
||||
)}
|
||||
config={config}
|
||||
options={options}
|
||||
onMessage={messageHandler}
|
||||
@ -115,6 +118,47 @@ const OnboardingSuccess = () => {
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="h5">Next Steps</Typography>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
marginTop={3}
|
||||
sx={{
|
||||
border: 1,
|
||||
borderColor: "grey.500",
|
||||
}}
|
||||
padding={5}
|
||||
>
|
||||
<Typography variant="body1" gutterBottom sx={{ p: 2 }}>
|
||||
For app publishers, await the start of the stage 1 chain, which will
|
||||
be announced in various social media channels. In the meantime,
|
||||
familiarize yourself with the{" "}
|
||||
<a
|
||||
href="https://github.com/hyphacoop/loro-testnet/blob/main/docs/publishing-webapps.md"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
webapp publishing workflow
|
||||
</a>{" "}
|
||||
as this is the main task you will be participating in.
|
||||
<br />
|
||||
<br />
|
||||
For validators, ensure your service provider is running and ready to
|
||||
deploy webapps. Await publication of the laconicd version, genesis
|
||||
file, and peer info to the LORO testnet repo, then follow{" "}
|
||||
<a
|
||||
href="https://git.vdb.to/cerc-io/testnet-laconicd-stack/src/branch/main/testnet-onboarding-validator.md#join-as-a-validator-on-stage1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
these instructions
|
||||
</a>{" "}
|
||||
for joining stage 1 as a validator/service provider.
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -2,6 +2,8 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
|
||||
import { Box, Divider, Typography } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton/LoadingButton";
|
||||
import {
|
||||
MsgOnboardParticipantEncodeObject,
|
||||
typeUrlMsgOnboardParticipant,
|
||||
@ -9,34 +11,38 @@ import {
|
||||
import { StargateClient } from "@cosmjs/stargate";
|
||||
|
||||
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
||||
import TermsAndConditionsCard, { Role } from "../components/TermsAndConditionsCard";
|
||||
import { Button } from "../components/ui/Button";
|
||||
import { BodyText } from "../components/ui/BodyText";
|
||||
import { Card } from "../components/ui/Card";
|
||||
import SelectRoleCard, { Role } from "../components/SelectRoleCard";
|
||||
import { HASHED_SUBSCRIBER_ID_KEY } from "../constants";
|
||||
import { Layout } from "../layout/Layout";
|
||||
import { CodeBlock } from "../components/CodeBlock";
|
||||
|
||||
const SignWithCosmos = () => {
|
||||
const { session, signClient } = useWalletConnectContext();
|
||||
|
||||
const location = useLocation();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [balance, setBalance] = useState('');
|
||||
const [requestState, setRequestState] = useState<null | "pending" | "success">(null);
|
||||
const [balance, setBalance] = useState("");
|
||||
const [isRequesting, setIsRequesting] = useState(false);
|
||||
const [isTncAccepted, setIsTncAccepted] = useState(false);
|
||||
const [role, setRole] = useState(Role.Participant);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {message: innerMessage, cosmosAddress, receivedEthSig: ethSignature, kycIdHash} = location.state as {
|
||||
const {
|
||||
message: innerMessage,
|
||||
cosmosAddress,
|
||||
receivedEthSig: ethSignature,
|
||||
} = location.state as {
|
||||
message?: {
|
||||
msg: string;
|
||||
address: string;
|
||||
};
|
||||
cosmosAddress?: string;
|
||||
receivedEthSig?: string;
|
||||
kycIdHash?: string;
|
||||
};
|
||||
|
||||
const ethAddress = innerMessage!.address;
|
||||
const subscriberIdHash = localStorage.getItem(HASHED_SUBSCRIBER_ID_KEY);
|
||||
|
||||
const createCosmosClient = useCallback(async (endpoint: string) => {
|
||||
return await StargateClient.connect(endpoint);
|
||||
@ -50,16 +56,18 @@ const SignWithCosmos = () => {
|
||||
participant: cosmosAddress!,
|
||||
ethPayload: innerMessage,
|
||||
ethSignature: ethSignature!,
|
||||
kycId: kycIdHash!,
|
||||
role
|
||||
kycId: subscriberIdHash!,
|
||||
role,
|
||||
},
|
||||
};
|
||||
}, [cosmosAddress, innerMessage, ethSignature, kycIdHash, role]);
|
||||
}, [cosmosAddress, innerMessage, ethSignature, subscriberIdHash, role]);
|
||||
|
||||
const handleTokenRequest = async () => {
|
||||
try {
|
||||
setRequestState("pending");
|
||||
const response = await fetch(`${process.env.REACT_APP_FAUCET_ENDPOINT!}/faucet`, {
|
||||
setIsRequesting(true);
|
||||
const response = await fetch(
|
||||
`${process.env.REACT_APP_FAUCET_ENDPOINT!}/faucet`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -67,15 +75,17 @@ const SignWithCosmos = () => {
|
||||
body: JSON.stringify({
|
||||
address: cosmosAddress,
|
||||
}),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
enqueueSnackbar("Tokens sent successfully", { variant: "success" });
|
||||
setRequestState("success");
|
||||
} else {
|
||||
const errorResponse = await response.json();
|
||||
if (response.status === 429) {
|
||||
enqueueSnackbar(`${response.statusText} : ${errorResponse.error}`, { variant: "error" });
|
||||
enqueueSnackbar(`${response.statusText} : ${errorResponse.error}`, {
|
||||
variant: "error",
|
||||
});
|
||||
} else {
|
||||
throw new Error(errorResponse.error);
|
||||
}
|
||||
@ -85,12 +95,13 @@ const SignWithCosmos = () => {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
enqueueSnackbar("Error getting tokens from faucet", { variant: "error" });
|
||||
setRequestState(null);
|
||||
} finally {
|
||||
setIsRequesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sendTransaction = async (
|
||||
transactionMessage: MsgOnboardParticipantEncodeObject
|
||||
transactionMessage: MsgOnboardParticipantEncodeObject,
|
||||
) => {
|
||||
if (!ethAddress) {
|
||||
enqueueSnackbar("Set nitro address");
|
||||
@ -100,6 +111,10 @@ const SignWithCosmos = () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
enqueueSnackbar("View and sign the message from your Laconic Wallet", {
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
const params = { transactionMessage, signer: cosmosAddress };
|
||||
const responseFromWallet = await signClient!.request<{
|
||||
code: number;
|
||||
@ -130,8 +145,13 @@ const SignWithCosmos = () => {
|
||||
|
||||
const getBalances = useCallback(async () => {
|
||||
try {
|
||||
const cosmosClient = await createCosmosClient(process.env.REACT_APP_LACONICD_RPC_ENDPOINT!);
|
||||
const balance = await cosmosClient.getBalance(cosmosAddress!, process.env.REACT_APP_LACONICD_DENOM!);
|
||||
const cosmosClient = await createCosmosClient(
|
||||
process.env.REACT_APP_LACONICD_RPC_ENDPOINT!,
|
||||
);
|
||||
const balance = await cosmosClient.getBalance(
|
||||
cosmosAddress!,
|
||||
process.env.REACT_APP_LACONICD_DENOM!,
|
||||
);
|
||||
setBalance(balance.amount);
|
||||
} catch (error) {
|
||||
console.error("Error fetching balance:", error);
|
||||
@ -146,74 +166,55 @@ const SignWithCosmos = () => {
|
||||
return (
|
||||
<>
|
||||
{!isTncAccepted && (
|
||||
<section>
|
||||
<h4>Please accept terms and conditions to continue</h4>
|
||||
<TermsAndConditionsCard
|
||||
className="mt-2"
|
||||
<Layout
|
||||
title="Please accept the terms and conditions to continue"
|
||||
noBackButton
|
||||
>
|
||||
<SelectRoleCard
|
||||
handleAccept={() => setIsTncAccepted(true)}
|
||||
handleRoleChange={setRole}
|
||||
/>
|
||||
</section>
|
||||
</Layout>
|
||||
)}
|
||||
|
||||
<section>
|
||||
<h4>Send transaction to chain</h4>
|
||||
<Card className="mt-2">
|
||||
<div className="flex flex-col items-stretch">
|
||||
<BodyText sizing={3}>Cosmos Account:</BodyText>
|
||||
<pre
|
||||
className="mt-2 px-4 py-3 bg-cream font-sans body-3"
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
}}
|
||||
>
|
||||
{`Address: ${cosmosAddress}\nBalance: ${balance || "…"} ${
|
||||
process.env.REACT_APP_LACONICD_DENOM
|
||||
}`}
|
||||
</pre>
|
||||
<Button
|
||||
variant={requestState === "success" ? "success" : "primary"}
|
||||
<Layout title="Send transaction to chain" noBackButton>
|
||||
<Typography>Laconic Account:</Typography>
|
||||
<Box sx={{ backgroundColor: "#29292E", p: 2, borderRadius: 1, mb: 2 }}>
|
||||
<Typography variant="body1">Address: {cosmosAddress}</Typography>
|
||||
<Typography variant="body1">
|
||||
Balance: {balance} {process.env.REACT_APP_LACONICD_DENOM}
|
||||
</Typography>
|
||||
</Box>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
onClick={handleTokenRequest}
|
||||
disabled={isTncAccepted ? !!requestState : !isTncAccepted}
|
||||
loading={requestState === "pending"}
|
||||
className="mt-6 md:self-start"
|
||||
disabled={isTncAccepted ? isRequesting : !isTncAccepted}
|
||||
loading={isRequesting}
|
||||
>
|
||||
{requestState === "success" ? "Tokens sent" : "Request tokens from Faucet"}
|
||||
</Button>
|
||||
</div>
|
||||
Request tokens from Faucet
|
||||
</LoadingButton>
|
||||
<Divider flexItem sx={{ my: 2 }} />
|
||||
<Typography variant="body1">Onboarding message:</Typography>
|
||||
|
||||
<div className="border-t border-neutral-30"></div>
|
||||
<CodeBlock>{JSON.stringify(onboardParticipantMsg, null, 2)} </CodeBlock>
|
||||
|
||||
<div>
|
||||
<BodyText sizing={3}>Onboarding message:</BodyText>
|
||||
<div
|
||||
className="mt-2 p-4 md:p-6 bg-neutral-30 body-3"
|
||||
style={{
|
||||
whiteSpace: "pre-wrap",
|
||||
wordWrap: "break-word",
|
||||
overflowWrap: "break-word",
|
||||
<Box
|
||||
sx={{
|
||||
paddingBottom: 2,
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
<pre style={{ whiteSpace: "pre-wrap", margin: 0 }}>
|
||||
{JSON.stringify(onboardParticipantMsg, null, 2)}{" "}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
onClick={async () => {
|
||||
await sendTransaction(onboardParticipantMsg);
|
||||
}}
|
||||
loading={isLoading}
|
||||
disabled={isTncAccepted ? balance === "0" : !isTncAccepted}
|
||||
className="md:self-start"
|
||||
>
|
||||
Send transaction
|
||||
</Button>
|
||||
</Card>
|
||||
</section>
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,27 +1,35 @@
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import canonicalStringify from "canonical-json";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
import { Select, MenuItem } from "@mui/material";
|
||||
import { Select, MenuItem, Box, Typography, Stack } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import { utf8ToHex } from "@walletconnect/encoding";
|
||||
|
||||
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
||||
import { ENABLE_KYC } from "../constants";
|
||||
import { Button } from "../components/ui/Button";
|
||||
import { Card } from "../components/ui/Card";
|
||||
import { ENABLE_KYC, HASHED_SUBSCRIBER_ID_KEY } from "../constants";
|
||||
import { Layout } from "../layout/Layout";
|
||||
import { CodeBlock } from "../components/CodeBlock";
|
||||
|
||||
const SignWithNitroKey = () => {
|
||||
const { session, signClient, checkPersistedState } = useWalletConnectContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (signClient && !session) {
|
||||
checkPersistedState(signClient);
|
||||
}
|
||||
}, [session, signClient, checkPersistedState]);
|
||||
const { session, signClient, isSessionLoading } = useWalletConnectContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isSessionLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
navigate("/connect-wallet?redirectTo=sign-with-nitro-key", {
|
||||
state: location.state,
|
||||
});
|
||||
}
|
||||
}, [session, isSessionLoading, navigate, location.state]);
|
||||
|
||||
const [ethAddress, setEthAddress] = useState("");
|
||||
const [ethSignature, setEthSignature] = useState("");
|
||||
|
||||
@ -29,6 +37,20 @@ const SignWithNitroKey = () => {
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const subscriberIdHash = useMemo(() => {
|
||||
return localStorage.getItem(HASHED_SUBSCRIBER_ID_KEY);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!subscriberIdHash) {
|
||||
setIsLoading(false);
|
||||
enqueueSnackbar(
|
||||
"Subscriber ID not found. Please verify your email and try again",
|
||||
{ variant: "error" },
|
||||
);
|
||||
}
|
||||
}, [subscriberIdHash]);
|
||||
|
||||
const message = useMemo(() => {
|
||||
return {
|
||||
msg: "Register my account as a participant on the Laconic network",
|
||||
@ -40,6 +62,11 @@ const SignWithNitroKey = () => {
|
||||
if (session && signClient) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
enqueueSnackbar("View and sign the message from your Laconic Wallet", {
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
const jsonMessage = canonicalStringify(message);
|
||||
const hexMsg = utf8ToHex(jsonMessage, true);
|
||||
const receivedEthSig: string = await signClient!.request({
|
||||
@ -62,14 +89,11 @@ const SignWithNitroKey = () => {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const kycIdHash = ethers.utils.sha256(ethers.utils.toUtf8Bytes(cosmosAddress));
|
||||
|
||||
navigate("/sign-with-cosmos", {
|
||||
state: {
|
||||
message,
|
||||
cosmosAddress,
|
||||
receivedEthSig,
|
||||
kycIdHash,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -82,15 +106,10 @@ const SignWithNitroKey = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Layout title="New Session" noBackButton>
|
||||
{session ? (
|
||||
<div>
|
||||
<h4>Sign with Nitro key</h4>
|
||||
|
||||
<Card className="mt-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<div className="body-3">Select Laconic account:</div>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="body1">Select Laconic account:</Typography>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
@ -98,10 +117,7 @@ const SignWithNitroKey = () => {
|
||||
onChange={(e: any) => {
|
||||
setCosmosAddress(e.target.value);
|
||||
}}
|
||||
className="mt-2 w-full"
|
||||
SelectDisplayProps={{
|
||||
className: "!bg-cream !py-2 !text-[14px] !font-light",
|
||||
}}
|
||||
style={{ maxWidth: "600px", display: "block" }}
|
||||
>
|
||||
{session?.namespaces.cosmos.accounts.map((address, index) => (
|
||||
<MenuItem value={address.split(":")[2]} key={index}>
|
||||
@ -109,10 +125,7 @@ const SignWithNitroKey = () => {
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="body-3">Select Nitro account:</div>
|
||||
<Typography variant="body1">Select Nitro account: </Typography>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
id="demo-simple-select"
|
||||
@ -120,10 +133,7 @@ const SignWithNitroKey = () => {
|
||||
onChange={(e: any) => {
|
||||
setEthAddress(e.target.value);
|
||||
}}
|
||||
className="mt-2 w-full"
|
||||
SelectDisplayProps={{
|
||||
className: "!bg-cream !py-2 !text-[14px] !font-light",
|
||||
}}
|
||||
style={{ maxWidth: "600px", display: "block" }}
|
||||
>
|
||||
{session?.namespaces.eip155.accounts.map((address, index) => (
|
||||
<MenuItem value={address.split(":")[2]} key={index}>
|
||||
@ -131,31 +141,26 @@ const SignWithNitroKey = () => {
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ethAddress && cosmosAddress && (
|
||||
<div className="p-4 md:p-6 break-words body-3 bg-neutral-30">
|
||||
<pre style={{ whiteSpace: "pre-wrap" }}>
|
||||
{canonicalStringify(message, null, 2)}{" "}
|
||||
</pre>
|
||||
</div>
|
||||
{Boolean(ethAddress) && Boolean(cosmosAddress) && (
|
||||
<CodeBlock>{canonicalStringify(message, null, 2)} </CodeBlock>
|
||||
)}
|
||||
|
||||
<Button
|
||||
<Box>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
onClick={signEth}
|
||||
disabled={!Boolean(ethAddress)}
|
||||
disabled={!Boolean(ethAddress) || !subscriberIdHash}
|
||||
sx={{ marginTop: 2 }}
|
||||
loading={isLoading}
|
||||
className="md:self-start"
|
||||
>
|
||||
Sign using Nitro key
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Stack>
|
||||
) : (
|
||||
<>Loading...</>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,33 +0,0 @@
|
||||
import React, { useNavigate } from "react-router-dom";
|
||||
|
||||
import { TNC_GENERIC_CONTENT } from "../constants";
|
||||
import { BodyText } from "../components/ui/BodyText";
|
||||
import { Button } from "../components/ui/Button";
|
||||
|
||||
const TermsAndConditions = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleAccept = () => {
|
||||
navigate("/connect-wallet");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-h-0 p-3 md:p-8">
|
||||
<div className="h-full md:mx-auto max-w-3xl px-4 md:px-6 py-6 md:py-8 flex flex-col gap-6 md:gap-8 bg-neutral-10">
|
||||
<div className="flex-1 flex flex-col gap-6 min-h-0">
|
||||
<h4>Terms and Conditions</h4>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<BodyText sizing={2}>
|
||||
<div dangerouslySetInnerHTML={{ __html: TNC_GENERIC_CONTENT }} />
|
||||
</BodyText>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button onClick={handleAccept}>ACCEPT</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsAndConditions;
|
72
src/pages/Thanks.tsx
Normal file
72
src/pages/Thanks.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
import { Box, colors, Typography } from '@mui/material';
|
||||
|
||||
import { HASHED_SUBSCRIBER_ID_KEY } from '../constants';
|
||||
|
||||
interface JwtPayload {
|
||||
subscriber_id: string;
|
||||
exp: number;
|
||||
iss: string;
|
||||
iat: number;
|
||||
}
|
||||
|
||||
const Thanks: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [err, setErr] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const token = queryParams.get('jwt_token');
|
||||
|
||||
try {
|
||||
if (!token) {
|
||||
throw new Error("Invalid JWT Token");
|
||||
}
|
||||
|
||||
const decoded = jwtDecode(token) as JwtPayload;
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
if (!decoded.subscriber_id) {
|
||||
throw new Error("Subscriber ID not found");
|
||||
}
|
||||
|
||||
if (decoded.exp < currentTime) {
|
||||
throw new Error("Token has expired");
|
||||
}
|
||||
|
||||
const subscriberIdBytes = ethers.utils.toUtf8Bytes(decoded.subscriber_id);
|
||||
const subscriberIdHash = ethers.utils.sha256(subscriberIdBytes);
|
||||
|
||||
localStorage.setItem(HASHED_SUBSCRIBER_ID_KEY, subscriberIdHash);
|
||||
|
||||
navigate('/sign-with-nitro-key');
|
||||
} catch (error) {
|
||||
setErr(String(error));
|
||||
}
|
||||
|
||||
}, [location.search, navigate]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
marginY={20}
|
||||
marginX={50}
|
||||
padding={5}
|
||||
>
|
||||
<Typography variant="h4" component="h1" gutterBottom color={colors.red[400]}>
|
||||
{err ? err : "Loading..."}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Thanks;
|
@ -45,13 +45,14 @@ const UserVerification = () => {
|
||||
useEffect(() => {
|
||||
if (applicationSubmitted && kycId !== '') {
|
||||
const kycIdHash = ethers.utils.sha256(ethers.utils.toUtf8Bytes(kycId));
|
||||
|
||||
navigate("/sign-with-cosmos", {
|
||||
state: {
|
||||
message,
|
||||
cosmosAddress,
|
||||
receivedEthSig,
|
||||
kycIdHash,
|
||||
}})
|
||||
kycIdHash
|
||||
}});
|
||||
}
|
||||
}, [applicationSubmitted, kycId, navigate, cosmosAddress, message, receivedEthSig]);
|
||||
|
||||
|
279
src/pages/Validator.tsx
Normal file
279
src/pages/Validator.tsx
Normal file
@ -0,0 +1,279 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { enqueueSnackbar } from "notistack";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MsgCreateValidator } from "cosmjs-types/cosmos/staking/v1beta1/tx";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Link,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { fromBech32, toBech32 } from "@cosmjs/encoding";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { EncodeObject, encodePubkey } from "@cosmjs/proto-signing";
|
||||
import { Registry } from "@cerc-io/registry-sdk";
|
||||
|
||||
import { useWalletConnectContext } from "../context/WalletConnectContext";
|
||||
import { Participant } from "../types";
|
||||
import { Layout } from "../layout/Layout";
|
||||
import { CodeBlock } from "../components/CodeBlock";
|
||||
|
||||
const Validator = () => {
|
||||
const { session, signClient, isSessionLoading } = useWalletConnectContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [cosmosAddress, setCosmosAddress] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [moniker, setMoniker] = useState("");
|
||||
const [pubKey, setPubKey] = useState("");
|
||||
const [participant, setParticipant] = useState<Participant | null>(null);
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSessionLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
navigate("/connect-wallet?redirectTo=validator");
|
||||
}
|
||||
}, [session, navigate, isSessionLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cosmosAddress) {
|
||||
setParticipant(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchParticipant = async () => {
|
||||
const registry = new Registry(
|
||||
process.env.REACT_APP_REGISTRY_GQL_ENDPOINT!,
|
||||
);
|
||||
|
||||
try {
|
||||
const fetchedParticipant =
|
||||
await registry.getParticipantByAddress(cosmosAddress);
|
||||
if (fetchedParticipant) {
|
||||
setParticipant(fetchedParticipant);
|
||||
} else {
|
||||
throw new Error("Participant not found");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching participant", error);
|
||||
enqueueSnackbar("Error in fetching participant", { variant: "error" });
|
||||
setParticipant(null);
|
||||
}
|
||||
};
|
||||
|
||||
fetchParticipant();
|
||||
}, [cosmosAddress]);
|
||||
|
||||
const isMonikerValid = useMemo(() => moniker.trim().length > 0, [moniker]);
|
||||
const isPubKeyValid = useMemo(() => pubKey.length === 44, [pubKey]);
|
||||
|
||||
const msgCreateValidator: MsgCreateValidator = useMemo(() => {
|
||||
const encodedPubKey = encodePubkey({
|
||||
type: "tendermint/PubKeyEd25519",
|
||||
value: pubKey.length === 44 ? pubKey : "",
|
||||
});
|
||||
|
||||
return {
|
||||
description: {
|
||||
moniker,
|
||||
identity: "",
|
||||
website: "",
|
||||
securityContact: "",
|
||||
details: "",
|
||||
},
|
||||
commission: {
|
||||
maxChangeRate: "10000000000000000", // 0.01
|
||||
maxRate: "200000000000000000", // 0.2
|
||||
rate: "100000000000000000", // 0.1
|
||||
},
|
||||
minSelfDelegation: "1",
|
||||
delegatorAddress: "",
|
||||
validatorAddress:
|
||||
cosmosAddress &&
|
||||
toBech32("laconicvaloper", fromBech32(cosmosAddress).data),
|
||||
pubkey: encodedPubKey,
|
||||
value: {
|
||||
amount: process.env.REACT_APP_STAKING_AMOUNT!,
|
||||
denom: process.env.REACT_APP_LACONICD_DENOM!,
|
||||
},
|
||||
};
|
||||
}, [cosmosAddress, pubKey, moniker]);
|
||||
|
||||
const msgCreateValidatorEncodeObject: EncodeObject = {
|
||||
typeUrl: "/cosmos.staking.v1beta1.MsgCreateValidator",
|
||||
value: MsgCreateValidator.toJSON(msgCreateValidator),
|
||||
};
|
||||
|
||||
const sendTransaction = async () => {
|
||||
if (
|
||||
!isMonikerValid ||
|
||||
!isPubKeyValid ||
|
||||
!msgCreateValidator.validatorAddress
|
||||
) {
|
||||
setIsError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
enqueueSnackbar("View and sign the message from your Laconic Wallet", {
|
||||
variant: "info",
|
||||
});
|
||||
|
||||
try {
|
||||
const params = {
|
||||
transactionMessage: msgCreateValidatorEncodeObject,
|
||||
signer: cosmosAddress,
|
||||
};
|
||||
const response = await signClient!.request<{ code: number }>({
|
||||
topic: session!.topic,
|
||||
chainId: `cosmos:${process.env.REACT_APP_LACONICD_CHAIN_ID}`,
|
||||
request: {
|
||||
method: "cosmos_sendTransaction",
|
||||
params,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error("Transaction not sent");
|
||||
} else {
|
||||
navigate("/validator-success", {
|
||||
state: { validatorAddress: msgCreateValidator.validatorAddress },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending transaction", error);
|
||||
enqueueSnackbar("Error in sending transaction", { variant: "error" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const replacer = (_key: string, value: any): any => {
|
||||
if (value instanceof Uint8Array) {
|
||||
return Buffer.from(value).toString("hex");
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout title="Create a validator">
|
||||
<Typography variant="body1">Select Laconic account:</Typography>
|
||||
<Select
|
||||
sx={{ marginBottom: 2 }}
|
||||
id="cosmos-address-select"
|
||||
value={cosmosAddress}
|
||||
onChange={(e) => setCosmosAddress(e.target.value)}
|
||||
style={{ maxWidth: "600px", display: "block" }}
|
||||
>
|
||||
{session?.namespaces.cosmos.accounts.map((address, index) => (
|
||||
<MenuItem value={address.split(":")[2]} key={index}>
|
||||
{address.split(":")[2]}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{Boolean(cosmosAddress) && (
|
||||
<>
|
||||
{participant ? (
|
||||
<Typography>Onboarded participant</Typography>
|
||||
) : (
|
||||
<Typography>No participant found</Typography>
|
||||
)}
|
||||
|
||||
{participant && (
|
||||
<CodeBlock>
|
||||
Laconic Address: {participant.cosmosAddress} <br />
|
||||
Nitro Address: {participant.nitroAddress} <br />
|
||||
Role: {participant.role} <br />
|
||||
KYC ID: {participant.kycId} <br />
|
||||
</CodeBlock>
|
||||
)}
|
||||
|
||||
{participant?.role === "validator" && (
|
||||
<>
|
||||
<Box style={{ maxWidth: "600px" }}>
|
||||
<TextField
|
||||
id="moniker"
|
||||
label="Enter your node moniker (example: AliceNode)"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
value={moniker}
|
||||
onChange={(e) => {
|
||||
setIsError(false);
|
||||
setMoniker(e.target.value);
|
||||
}}
|
||||
error={!isMonikerValid && isError}
|
||||
helperText={
|
||||
!isMonikerValid && isError ? "Moniker is required" : ""
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ marginTop: 3 }}>
|
||||
Fetch your validator public key using the following command
|
||||
(refer
|
||||
<Link
|
||||
href="https://git.vdb.to/cerc-io/testnet-laconicd-stack/src/branch/main/testnet-onboarding-validator.md#join-as-testnet-validator"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
this guide
|
||||
</Link>
|
||||
)
|
||||
</Typography>
|
||||
|
||||
<CodeBlock>
|
||||
{`laconic-so deployment --dir testnet-laconicd-deployment exec laconicd "laconicd cometbft show-validator" | jq -r .key`}
|
||||
</CodeBlock>
|
||||
|
||||
<Box sx={{ maxWidth: "600px" }}>
|
||||
<TextField
|
||||
id="pub-key"
|
||||
label="Enter your validator public key"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
margin="normal"
|
||||
value={pubKey}
|
||||
onChange={(e) => {
|
||||
setIsError(false);
|
||||
setPubKey(e.target.value);
|
||||
}}
|
||||
error={!isPubKeyValid && isError}
|
||||
helperText={
|
||||
!isPubKeyValid && isError
|
||||
? "Public key must be 44 characters"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Typography>Send transaction to chain</Typography>
|
||||
<CodeBlock>
|
||||
{JSON.stringify(msgCreateValidator, replacer, 2)}
|
||||
</CodeBlock>
|
||||
<Box marginTop={1} marginBottom={1}>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
onClick={sendTransaction}
|
||||
loading={isLoading}
|
||||
disabled={isError}
|
||||
>
|
||||
Send transaction
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Validator;
|
35
src/pages/ValidatorSuccess.tsx
Normal file
35
src/pages/ValidatorSuccess.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
import { Link, Typography } from "@mui/material";
|
||||
import { Layout } from "../layout/Layout";
|
||||
import { CodeBlock } from "../components/CodeBlock";
|
||||
|
||||
const ValidatorSuccess = () => {
|
||||
const location = useLocation();
|
||||
const { validatorAddress } = location.state as {
|
||||
validatorAddress?: string;
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout title="Validator created successfully">
|
||||
<Typography sx={{ marginTop: 3 }}>
|
||||
You can view your validator details using the following command
|
||||
(Refer
|
||||
<Link
|
||||
href="https://git.vdb.to/cerc-io/testnet-laconicd-stack/src/branch/main/testnet-onboarding-validator.md#join-as-testnet-validator"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
this guide
|
||||
</Link>
|
||||
)
|
||||
</Typography>
|
||||
<CodeBlock>
|
||||
{`laconic-so deployment --dir testnet-laconicd-deployment exec laconicd "laconicd query staking validators --output json" | jq '.validators[] | select(.operator_address == "${validatorAddress}")'`}
|
||||
</CodeBlock>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ValidatorSuccess;
|
24
src/pages/VerifyEmail.tsx
Normal file
24
src/pages/VerifyEmail.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
const VerifyEmail = () => {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', margin: '20vh' }}>
|
||||
<iframe
|
||||
title="verify-email"
|
||||
src="https://embeds.beehiiv.com/18aaa245-3652-4b0a-94a9-a87054df4914"
|
||||
data-test-id="beehiiv-embed"
|
||||
width="480"
|
||||
height="320"
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
style={{
|
||||
borderRadius: '4px',
|
||||
border: '2px solid #e5e7eb',
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
></iframe>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default VerifyEmail;
|
6
src/types.ts
Normal file
6
src/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface Participant {
|
||||
cosmosAddress: string;
|
||||
nitroAddress: string;
|
||||
role: string;
|
||||
kycId: string;
|
||||
}
|
@ -29,5 +29,5 @@ export const getAccessTokenExpirationHandler = (userId: string) => {
|
||||
return async () => {
|
||||
const newToken = await fetchAccessToken(userId);
|
||||
return newToken;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,71 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/**/*.tsx',
|
||||
'./public/index.html',
|
||||
],
|
||||
theme: {
|
||||
colors: {
|
||||
cream: '#FBFBFB',
|
||||
black: '#0F0F0F', // Infinity Black
|
||||
neutral: {
|
||||
10: '#F1F1F2',
|
||||
20: '#E6E6E8',
|
||||
30: '#DADADE',
|
||||
40: '#CCCBD0',
|
||||
50: '#BDBCC3',
|
||||
60: '#A7A6AF',
|
||||
70: '#83828F',
|
||||
80: '#48474F',
|
||||
90: '#29292E',
|
||||
100: '#18181A',
|
||||
},
|
||||
primary: {
|
||||
10: '#F2F2FF',
|
||||
20: '#CACAFF',
|
||||
30: '#A2A2FF',
|
||||
40: '#7A7AFF',
|
||||
50: '#4545FF',
|
||||
60: '#0000F4',
|
||||
70: '#0000BE',
|
||||
80: '#000088',
|
||||
90: '#000051',
|
||||
100: '#000036',
|
||||
},
|
||||
danger: {
|
||||
10: '#FFF2F3',
|
||||
20: '#FFC9CC',
|
||||
30: '#FFA3A8',
|
||||
40: '#FF7A81',
|
||||
50: '#B20710',
|
||||
60: '#870007',
|
||||
},
|
||||
success: {
|
||||
10: '#F2FFF6',
|
||||
20: '#C9FFD9',
|
||||
30: '#7AFFA1',
|
||||
40: '#24A148',
|
||||
},
|
||||
warning: {
|
||||
10: '#FFFBF2',
|
||||
20: '#FFEFC9',
|
||||
30: '#E5AD29',
|
||||
40: '#A17203',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [`'TT Hoves', sans-serif`, {
|
||||
fontFeatureSettings: `'liga' off, 'clig' off`,
|
||||
}],
|
||||
mono: `"DM Mono", monospace`,
|
||||
},
|
||||
|
||||
extend: {
|
||||
boxShadow: {
|
||||
'button-primary': `0px 0px 20px 0px rgba(0, 0, 244, 0.50);`
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user