Integrate Beehiiv email verification in onboarding flow (#13)

Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)
- Add beehiiv widget for email verification
- Extract subscriber ID from the JWT
- Hash subscriber ID to be used as KYC ID

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/testnet-onboarding-app#13
This commit is contained in:
nabarun 2024-08-08 10:06:34 +00:00
parent 11f872032e
commit c041f031dc
12 changed files with 169 additions and 42 deletions

View File

@ -23,6 +23,7 @@
"crypto-browserify": "^3.12.0", "crypto-browserify": "^3.12.0",
"ethers": "5.7.2", "ethers": "5.7.2",
"https-browserify": "^1.0.0", "https-browserify": "^1.0.0",
"jwt-decode": "^4.0.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@ -11,6 +11,9 @@ import UserVerification from "./pages/UserVerification";
import TermsAndConditions from "./pages/TermsAndConditions"; import TermsAndConditions from "./pages/TermsAndConditions";
import Header from "./components/Header"; import Header from "./components/Header";
import { WalletConnectProvider } from "./context/WalletConnectContext"; import { WalletConnectProvider } from "./context/WalletConnectContext";
import VerifyEmail from "./pages/VerifyEmail";
import Email from "./pages/Email";
import Thanks from "./pages/Thanks";
function App() { function App() {
return ( return (
@ -19,7 +22,10 @@ function App() {
<WalletConnectProvider> <WalletConnectProvider>
<Routes> <Routes>
<Route path="/" element={<TermsAndConditions />} /> <Route path="/" element={<TermsAndConditions />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/email" element={<Email/>} />
<Route path="/connect-wallet" element={<ConnectWallet />} /> <Route path="/connect-wallet" element={<ConnectWallet />} />
<Route path="/thanks" element={<Thanks />} />
<Route element={<SignPageLayout />}> <Route element={<SignPageLayout />}>
<Route path="/sign-with-nitro-key" element={<SignWithNitroKey />} /> <Route path="/sign-with-nitro-key" element={<SignWithNitroKey />} />
<Route <Route

View File

@ -30,4 +30,6 @@ export const TNC_PARTICIPANT_CONTENT = `Lorem ipsum dolor sit amet, consectetur
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 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 = 'Please check your inbox and click the link to verify your email address.'
export const ENABLE_KYC = false; export const ENABLE_KYC = false;

View File

@ -1,5 +1,5 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import {useNavigate } from "react-router-dom"; import {useLocation, useNavigate } from "react-router-dom";
import { Button, Box, Container, Typography, colors } from "@mui/material"; import { Button, Box, Container, Typography, colors } from "@mui/material";
@ -10,14 +10,16 @@ const ConnectWallet = () => {
const { connect, session } = useWalletConnectContext(); const { connect, session } = useWalletConnectContext();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
useEffect(() => { useEffect(() => {
if (session) { if (session) {
navigate("/sign-with-nitro-key"); navigate("/sign-with-nitro-key", {
state: location.state
});
} }
}, [session, navigate,]); }, [session, navigate, location]);
const handler = async () => { const handler = async () => {
await connect(); await connect();

33
src/pages/Email.tsx Normal file
View 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

View File

@ -25,14 +25,14 @@ const SignWithCosmos = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const {message: innerMessage, cosmosAddress, receivedEthSig: ethSignature, kycIdHash} = location.state as { const {message: innerMessage, cosmosAddress, receivedEthSig: ethSignature, subscriberIdHash} = location.state as {
message?: { message?: {
msg: string; msg: string;
address: string; address: string;
}; };
cosmosAddress?: string; cosmosAddress?: string;
receivedEthSig?: string; receivedEthSig?: string;
kycIdHash?: string; subscriberIdHash?: string;
}; };
const ethAddress = innerMessage!.address; const ethAddress = innerMessage!.address;
@ -49,11 +49,11 @@ const SignWithCosmos = () => {
participant: cosmosAddress!, participant: cosmosAddress!,
ethPayload: innerMessage, ethPayload: innerMessage,
ethSignature: ethSignature!, ethSignature: ethSignature!,
kycId: kycIdHash!, kycId: subscriberIdHash!,
role role
}, },
}; };
}, [cosmosAddress, innerMessage, ethSignature, kycIdHash, role]); }, [cosmosAddress, innerMessage, ethSignature, subscriberIdHash, role]);
const handleTokenRequest = async () => { const handleTokenRequest = async () => {
try { try {

View File

@ -1,8 +1,7 @@
import React, { useState, useMemo, useEffect } from "react"; 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 { enqueueSnackbar } from "notistack";
import canonicalStringify from "canonical-json"; import canonicalStringify from "canonical-json";
import { ethers } from "ethers";
import { import {
Select, Select,
@ -28,6 +27,7 @@ const SignWithNitroKey = () => {
}, [session, signClient, checkPersistedState]); }, [session, signClient, checkPersistedState]);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [ethAddress, setEthAddress] = useState(""); const [ethAddress, setEthAddress] = useState("");
const [ethSignature, setEthSignature] = useState(""); const [ethSignature, setEthSignature] = useState("");
@ -68,14 +68,20 @@ const SignWithNitroKey = () => {
}, },
}); });
} else { } else {
const kycIdHash = ethers.utils.sha256(ethers.utils.toUtf8Bytes(cosmosAddress)); const state = location.state as {
subscriberIdHash?: string
}
if (!state.subscriberIdHash) {
throw new Error("Subscriber ID not found. Please verify your email and try again")
}
navigate("/sign-with-cosmos", { navigate("/sign-with-cosmos", {
state: { state: {
message, message,
cosmosAddress, cosmosAddress,
receivedEthSig, receivedEthSig,
kycIdHash, subscriberIdHash: state.subscriberIdHash,
}, },
}); });
} }

View File

@ -9,7 +9,7 @@ const TermsAndConditions = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const handleAccept = () => { const handleAccept = () => {
navigate('/connect-wallet'); navigate('/verify-email');
}; };
return ( return (

72
src/pages/Thanks.tsx Normal file
View 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';
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);
navigate('/connect-wallet', {
state:{
subscriberIdHash
}
});
} 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;

View File

@ -50,7 +50,7 @@ const UserVerification = () => {
message, message,
cosmosAddress, cosmosAddress,
receivedEthSig, receivedEthSig,
kycIdHash, subscriberIdHash: kycIdHash,
}}) }})
} }
}, [applicationSubmitted, kycId, navigate, cosmosAddress, message, receivedEthSig]); }, [applicationSubmitted, kycId, navigate, cosmosAddress, message, receivedEthSig]);

24
src/pages/VerifyEmail.tsx Normal file
View 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

View File

@ -9032,6 +9032,11 @@ junk@3.1.0:
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
jwt-decode@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b"
integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==
keccak@^3.0.0: keccak@^3.0.0:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.4.tgz#edc09b89e633c0549da444432ecf062ffadee86d"
@ -11985,16 +11990,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -12078,14 +12074,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1" is-obj "^1.0.1"
is-regexp "^1.0.0" is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@ -13338,7 +13327,8 @@ workbox-window@6.6.1:
"@types/trusted-types" "^2.0.2" "@types/trusted-types" "^2.0.2"
workbox-core "6.6.1" workbox-core "6.6.1"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
name wrap-ansi-cjs
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@ -13356,15 +13346,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0" string-width "^4.1.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0: wrap-ansi@^8.1.0:
version "8.1.0" version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"