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:
parent
11f872032e
commit
c041f031dc
@ -23,6 +23,7 @@
|
||||
"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",
|
||||
|
@ -11,6 +11,9 @@ import UserVerification from "./pages/UserVerification";
|
||||
import TermsAndConditions from "./pages/TermsAndConditions";
|
||||
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";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@ -19,7 +22,10 @@ function App() {
|
||||
<WalletConnectProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<TermsAndConditions />} />
|
||||
<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
|
||||
|
@ -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 REDIRECT_EMAIL_MSG = 'Please check your inbox and click the link to verify your email address.'
|
||||
|
||||
export const ENABLE_KYC = false;
|
||||
|
@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
@ -10,14 +10,16 @@ const ConnectWallet = () => {
|
||||
const { connect, session } = useWalletConnectContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (session) {
|
||||
navigate("/sign-with-nitro-key");
|
||||
navigate("/sign-with-nitro-key", {
|
||||
state: location.state
|
||||
});
|
||||
}
|
||||
}, [session, navigate,]);
|
||||
}, [session, navigate, location]);
|
||||
|
||||
const handler = async () => {
|
||||
await connect();
|
||||
|
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
|
@ -25,14 +25,14 @@ const SignWithCosmos = () => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {message: innerMessage, cosmosAddress, receivedEthSig: ethSignature, kycIdHash} = location.state as {
|
||||
const {message: innerMessage, cosmosAddress, receivedEthSig: ethSignature, subscriberIdHash} = location.state as {
|
||||
message?: {
|
||||
msg: string;
|
||||
address: string;
|
||||
};
|
||||
cosmosAddress?: string;
|
||||
receivedEthSig?: string;
|
||||
kycIdHash?: string;
|
||||
subscriberIdHash?: string;
|
||||
};
|
||||
|
||||
const ethAddress = innerMessage!.address;
|
||||
@ -49,11 +49,11 @@ const SignWithCosmos = () => {
|
||||
participant: cosmosAddress!,
|
||||
ethPayload: innerMessage,
|
||||
ethSignature: ethSignature!,
|
||||
kycId: kycIdHash!,
|
||||
kycId: subscriberIdHash!,
|
||||
role
|
||||
},
|
||||
};
|
||||
}, [cosmosAddress, innerMessage, ethSignature, kycIdHash, role]);
|
||||
}, [cosmosAddress, innerMessage, ethSignature, subscriberIdHash, role]);
|
||||
|
||||
const handleTokenRequest = async () => {
|
||||
try {
|
||||
|
@ -1,8 +1,7 @@
|
||||
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,
|
||||
@ -28,6 +27,7 @@ const SignWithNitroKey = () => {
|
||||
}, [session, signClient, checkPersistedState]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [ethAddress, setEthAddress] = useState("");
|
||||
const [ethSignature, setEthSignature] = useState("");
|
||||
|
||||
@ -68,14 +68,20 @@ const SignWithNitroKey = () => {
|
||||
},
|
||||
});
|
||||
} 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", {
|
||||
state: {
|
||||
message,
|
||||
cosmosAddress,
|
||||
receivedEthSig,
|
||||
kycIdHash,
|
||||
subscriberIdHash: state.subscriberIdHash,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ const TermsAndConditions = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleAccept = () => {
|
||||
navigate('/connect-wallet');
|
||||
navigate('/verify-email');
|
||||
};
|
||||
|
||||
return (
|
||||
|
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';
|
||||
|
||||
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;
|
@ -50,7 +50,7 @@ const UserVerification = () => {
|
||||
message,
|
||||
cosmosAddress,
|
||||
receivedEthSig,
|
||||
kycIdHash,
|
||||
subscriberIdHash: kycIdHash,
|
||||
}})
|
||||
}
|
||||
}, [applicationSubmitted, kycId, navigate, cosmosAddress, message, receivedEthSig]);
|
||||
|
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
|
37
yarn.lock
37
yarn.lock
@ -9032,6 +9032,11 @@ junk@3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
|
||||
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:
|
||||
version "3.0.4"
|
||||
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"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
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:
|
||||
"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==
|
||||
@ -12078,14 +12074,7 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm: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:
|
||||
"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==
|
||||
@ -13338,7 +13327,8 @@ workbox-window@6.6.1:
|
||||
"@types/trusted-types" "^2.0.2"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
@ -13356,15 +13346,6 @@ wrap-ansi@^6.2.0:
|
||||
string-width "^4.1.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:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
|
||||
|
Loading…
Reference in New Issue
Block a user