Integrate sumsub KYC with terms & conditions #6

Merged
nabarun merged 5 commits from deep-stack/testnet-onboarding-app:kyc-integration into main 2024-07-30 08:22:10 +00:00
12 changed files with 259 additions and 66 deletions
Showing only changes of commit a875673189 - Show all commits

View File

@ -6,28 +6,32 @@ import SignWithNitroKey from "./pages/SignWithNitroKey";
import SignWithCosmos from "./pages/SignWithCosmos";
import PageNotFound from "./pages/PageNotFound";
import OnboardingSuccess from "./pages/OnboardingSuccess";
import UserVerification from "./pages/UserVerification";
import SignPageLayout from "./layout/SignPageLayout";
import UserVerification from "./pages/UserVerification";
import TermsAndConditions from "./pages/TermsAndConditions";
import Header from "./components/Header";
import { WalletConnectProvider } from "./context/WalletConnectContext";
function App() {
return (
<Router>
<Header />
<WalletConnectProvider>
<Routes>
<Route path="/" element={<ConnectWallet />} />
<Route path="/" element={<TermsAndConditions />} />
<Route
path="/user-verification"
element={<UserVerification />}
/>
<Route path="/connect-wallet" element={<ConnectWallet />} />
<Route element={<SignPageLayout />}>
<Route path="/sign-with-nitro-key/:userId" element={<SignWithNitroKey />} />
<Route path="/sign-with-nitro-key" element={<SignWithNitroKey />} />
<Route
path="/sign-with-cosmos/:cosmosAddress/:ethSignature"
path="/sign-with-cosmos"
element={<SignWithCosmos />}
/>
<Route
path="/onboarding-success/:cosmosAddress"
path="/onboarding-success"
element={<OnboardingSuccess />}
></Route>
</Route>

33
src/components/Header.tsx Normal file
View File

@ -0,0 +1,33 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { AppBar, Toolbar, Avatar, Box, IconButton } from '@mui/material';
const Header: React.FC = () => {
const location = useLocation()
return (
<AppBar position="static" color="inherit">
<Toolbar>
<Link to={location.pathname === "/" ? "/" : "/connect-wallet"} style={{ color: "inherit", textDecoration: "none" }}>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Avatar
alt="Laconic logo"
src="https://avatars.githubusercontent.com/u/92608123"
/>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
sx={{ ml: 2, mr: 2 }}
>
Testnet Onboarding
</IconButton>
</Box>
</Link>
</Toolbar>
</AppBar>
);
};
export default Header;

View File

@ -0,0 +1,81 @@
import React, { useState } from 'react';
import { Typography, Button, Box, Paper, Radio, RadioGroup, FormControlLabel, FormControl, FormLabel, Link, Checkbox } from '@mui/material';
import { Role } from '@cerc-io/registry-sdk/dist/proto/cerc/onboarding/v1/onboarding';
import TermsAndConditionsDialog from './TermsAndConditionsDialog';
const VALIDATOR_OPTION = "validator";
const PARTICIPANT_OPTION = "participant";
const TermsAndConditionsCard = ({ handleAccept, handleRoleChange }: { handleAccept: () => void, handleRoleChange: (role: Role) => void }) => {
const [selectedRole, setSelectedRole] = useState(PARTICIPANT_OPTION);
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 as HTMLInputElement).value);
handleRoleChange((event.target as HTMLInputElement).value === VALIDATOR_OPTION ? Role.ROLE_VALIDATOR : Role.ROLE_PARTICIPANT);
};
return (
<Paper elevation={3} style={{ padding: '2rem', marginTop: '2rem', display: isHidden ? "none" : "block" }}>
<FormControl component="fieldset">
<FormLabel component="legend">Select your role</FormLabel>
<RadioGroup
aria-label="roles"
name="roles"
value={selectedRole}
onChange={handleRadioChange}
>
<FormControlLabel
value={VALIDATOR_OPTION}
control={<Radio />}
label="Validator"
/>
<FormControlLabel
value={PARTICIPANT_OPTION}
control={<Radio />}
label="Participant"
/>
</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 isValidator = { selectedRole === VALIDATOR_OPTION } open={isDialogOpen} onClose={() => setisDialogOpen(false)}/>
</Paper>
);
};
export default TermsAndConditionsCard;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button, Typography } from '@mui/material';
import { TNC_CONTENT } from '../constants';
interface TermsDialogProps {
isValidator: boolean;
open: boolean;
onClose: () => void;
}
const TermsAndConditionsDialog: React.FC<TermsDialogProps> = ({ isValidator, open, onClose }) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>Terms and Conditions</DialogTitle>
<DialogContent>
<Typography variant="h6" gutterBottom>
Onboard as a {isValidator ? "validator" : "participant"}
</Typography>
<DialogContentText>
<div dangerouslySetInnerHTML={{__html: TNC_CONTENT}} />
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Close
</Button>
</DialogActions>
</Dialog>
);
};
export default TermsAndConditionsDialog;

9
src/constants.ts Normal file
View File

@ -0,0 +1,9 @@
export const TNC_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.`;

View File

@ -1,14 +1,12 @@
import React from "react";
import { Outlet, useNavigate, Link } from "react-router-dom";
import { Outlet, useNavigate } from "react-router-dom";
import {
Toolbar,
IconButton,
Avatar,
Button,
Typography,
Container,
Box,
Container
} from "@mui/material";
import { useWalletConnectContext } from "../context/WalletConnectContext";
@ -25,22 +23,6 @@ const SignPageLayout = () => {
return (
<>
<Toolbar variant="dense">
<Link to="/" style={{ color: "inherit", textDecoration: "none" }}>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Avatar
alt="Laconic logo"
src="https://avatars.githubusercontent.com/u/92608123"
/>
<IconButton
edge="start"
color="inherit"
aria-label="menu"
sx={{ ml: 2, mr: 2 }}
>
Testnet Onboarding
</IconButton>
</Box>
</Link>
<Button
variant="outlined"

View File

@ -1,7 +1,7 @@
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import { Typography, Button, Box, Container, Avatar } from "@mui/material";
import { Button, Box, Container } from "@mui/material";
import { useWalletConnectContext } from "../context/WalletConnectContext";
@ -10,11 +10,16 @@ const ConnectWallet = () => {
const navigate = useNavigate();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const kycId = queryParams.get('kycId');
useEffect(() => {
if (session) {
navigate("/user-verification");
navigate(`/sign-with-nitro-key?kycId=${kycId}`);
}
}, [session, navigate]);
}, [session, navigate, kycId]);
const handler = async () => {
await connect();
@ -30,28 +35,12 @@ const ConnectWallet = () => {
justifyContent="center"
padding={5}
>
<Box display="flex" alignItems="center">
<Avatar
alt="Laconic logo"
src="https://avatars.githubusercontent.com/u/92608123"
/>
<Typography
variant="h4"
component="h6"
style={{ marginLeft: "10px" }}
>
Testnet Onboarding
</Typography>
</Box>
<Typography variant="h6" component="h6" style={{ marginTop: "30px" }}>
Connect wallet
</Typography>
<Button
variant="contained"
onClick={handler}
style={{ marginTop: "20px" }}
>
Connect
Connect Wallet
</Button>
</Box>
</Container>

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { SnackbarProvider, enqueueSnackbar } from "notistack";
import { useParams } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { Box, Typography } from "@mui/material";
import { Registry } from "@cerc-io/registry-sdk";
@ -15,7 +15,9 @@ const registry = new Registry(
);
const OnboardingSuccess = () => {
const { cosmosAddress } = useParams();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const cosmosAddress = queryParams.get('cosmosAddress');
const [participant, setParticipant] = useState<Participant>();

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useParams, useLocation, useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import { SnackbarProvider, enqueueSnackbar } from "notistack";
import { Box, Card, CardContent, Grid, Typography } from "@mui/material";
@ -8,21 +8,28 @@ import {
MsgOnboardParticipantEncodeObject,
typeUrlMsgOnboardParticipant,
} from "@cerc-io/registry-sdk";
import { Role } from "@cerc-io/registry-sdk/dist/proto/cerc/onboarding/v1/onboarding";
import { StargateClient } from "@cosmjs/stargate";
import { useWalletConnectContext } from "../context/WalletConnectContext";
import TermsAndConditionsCard from "../components/TermsAndConditionsCard";
const SignWithCosmos = () => {
const { session, signClient } = useWalletConnectContext();
const { cosmosAddress, ethSignature } = useParams();
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const cosmosAddress = queryParams.get('cosmosAddress');
const ethSignature = queryParams.get('ethSignature');
const kycId = queryParams.get('kycId');
const [isLoading, setIsLoading] = useState(false);
const [balance, setBalance] = useState('');
const [isRequesting, setIsRequesting] = useState(false);
const [isTncAccepted, setIsTncAccepted] = useState(false);
const [role, setRole] = useState(Role.ROLE_PARTICIPANT);
const navigate = useNavigate();
const location = useLocation();
const innerMessage = location.state;
const ethAddress = innerMessage.address;
@ -35,12 +42,14 @@ const SignWithCosmos = () => {
return {
typeUrl: typeUrlMsgOnboardParticipant,
value: {
participant: cosmosAddress,
participant: cosmosAddress!,
ethPayload: innerMessage,
ethSignature,
ethSignature: ethSignature!,
kycId: kycId!,
role
},
};
}, [cosmosAddress, innerMessage, ethSignature]);
}, [cosmosAddress, innerMessage, ethSignature, kycId, role]);
const handleTokenRequest = async () => {
try {
@ -100,7 +109,7 @@ const SignWithCosmos = () => {
if (responseFromWallet.code !== 0) {
enqueueSnackbar("Transaction not sent", { variant: "error" });
} else {
navigate(`/onboarding-success/${cosmosAddress}`);
navigate(`/onboarding-success?cosmosAddress=${cosmosAddress}`);
}
} catch (error) {
console.error(error);
@ -130,10 +139,12 @@ const SignWithCosmos = () => {
sx={{
display: "flex",
flexDirection: "column",
marginTop: "100px",
marginY: "100px",
gap: "10px",
}}
>
<Typography variant="h5">Please accept terms and conditions to continue</Typography>
<TermsAndConditionsCard handleAccept={() => setIsTncAccepted(true)} handleRoleChange={setRole}/>
<Typography variant="h5">Send transaction to chain</Typography>
<Typography>Cosmos Account:</Typography>
<Card className='mt-1 mb-1'>
@ -147,7 +158,7 @@ const SignWithCosmos = () => {
<LoadingButton
variant="contained"
onClick={handleTokenRequest}
disabled={isRequesting}
disabled={isTncAccepted ? isRequesting : !isTncAccepted}
loading={isRequesting}
>
Request tokens from Faucet
@ -178,7 +189,7 @@ const SignWithCosmos = () => {
await sendTransaction(onboardParticipantMsg);
}}
loading={isLoading}
disabled={balance === '0'}
disabled={isTncAccepted ? (balance === '0') : !isTncAccepted}
>
Send transaction
</LoadingButton>

View File

@ -1,5 +1,5 @@
import React, { useState, useMemo, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import { SnackbarProvider, enqueueSnackbar } from "notistack";
import canonicalStringify from "canonical-json";
@ -15,6 +15,10 @@ import { utf8ToHex } from "@walletconnect/encoding";
import { useWalletConnectContext } from "../context/WalletConnectContext";
const SignWithNitroKey = () => {
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const kycId = queryParams.get('kycId');
const { session, signClient, checkPersistedState } =
useWalletConnectContext();
@ -55,7 +59,7 @@ const SignWithNitroKey = () => {
});
setIsLoading(false)
setEthSignature(ethSignature);
navigate(`/sign-with-cosmos/${cosmosAddress}/${receivedEthSig}`, {
navigate(`/sign-with-cosmos?cosmosAddress=${cosmosAddress}&ethSignature=${receivedEthSig}&kycId=${kycId}`, {
state: message,
});
} catch (error) {
@ -129,7 +133,7 @@ const SignWithNitroKey = () => {
style={{ marginTop: "20px" }}
loading={isLoading}
>
Sign using Nitro key
Sign using Nitro key
</LoadingButton>
</Box>
<SnackbarProvider />

View File

@ -0,0 +1,44 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Container, Typography, Button, Box, Paper } from '@mui/material';
import { TNC_CONTENT } from '../constants';
const TermsAndConditions = () => {
const navigate = useNavigate();
const handleAccept = () => {
navigate('/user-verification');
};
return (
<Container maxWidth="md">
<Paper elevation={3} style={{ padding: '2rem', marginTop: '2rem', height: '80vh', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h4" gutterBottom>
Terms and Conditions
</Typography>
<Box
style={{
overflowY: 'auto',
flexGrow: 1,
paddingRight: '1rem',
marginBottom: '1rem',
}}
>
<Typography variant="body1" gutterBottom>
<div dangerouslySetInnerHTML={{__html: TNC_CONTENT}} />
</Typography>
</Box>
<Box mt={2} display="flex" justifyContent="center">
<Button variant="contained" color="primary" onClick={handleAccept}>
Accept
</Button>
</Box>
</Paper>
</Container>
);
};
export default TermsAndConditions;

View File

@ -15,16 +15,16 @@ const options = {
}
const UserVerification = () => {
const [userId, setUserId] = useState<String>('');
const [kycId, setKycId] = useState<String>('');
const [applicationSubmitted, setApplicationSubmitted] = useState<boolean>(false);
const navigate = useNavigate();
useEffect(()=>{
if (applicationSubmitted && userId !== '') {
navigate(`/sign-with-nitro-key/${userId}`)
if (applicationSubmitted && kycId !== '') {
navigate(`/connect-wallet?kycId=${kycId}`)
}
}, [applicationSubmitted, userId, navigate]);
}, [applicationSubmitted, kycId, navigate]);
// TODO: Implement
const accessTokenExpirationHandler = async () => {
@ -41,7 +41,7 @@ const UserVerification = () => {
const messageHandler = (event: any, payload: any ) => {
if (event === 'idCheck.onApplicantLoaded') {
setUserId(payload.applicantId);
setKycId(payload.applicantId);
}
if (event === 'idCheck.onApplicantSubmitted'){