From c041f031dcd8cde4a69d76f92d81622bc887092f Mon Sep 17 00:00:00 2001 From: nabarun Date: Thu, 8 Aug 2024 10:06:34 +0000 Subject: [PATCH] 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 Reviewed-on: https://git.vdb.to/cerc-io/testnet-onboarding-app/pulls/13 --- package.json | 1 + src/App.tsx | 6 +++ src/constants.ts | 2 + src/pages/ConnectWallet.tsx | 10 +++-- src/pages/Email.tsx | 33 +++++++++++++++ src/pages/SignWithCosmos.tsx | 8 ++-- src/pages/SignWithNitroKey.tsx | 14 +++++-- src/pages/TermsAndConditions.tsx | 2 +- src/pages/Thanks.tsx | 72 ++++++++++++++++++++++++++++++++ src/pages/UserVerification.tsx | 2 +- src/pages/VerifyEmail.tsx | 24 +++++++++++ yarn.lock | 37 ++++------------ 12 files changed, 169 insertions(+), 42 deletions(-) create mode 100644 src/pages/Email.tsx create mode 100644 src/pages/Thanks.tsx create mode 100644 src/pages/VerifyEmail.tsx diff --git a/package.json b/package.json index f821887..7a76c72 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/App.tsx b/src/App.tsx index 596a474..4737a39 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> + } /> + } /> } /> + } /> }> } /> { 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(); diff --git a/src/pages/Email.tsx b/src/pages/Email.tsx new file mode 100644 index 0000000..630145d --- /dev/null +++ b/src/pages/Email.tsx @@ -0,0 +1,33 @@ +import React from 'react' + +import { Box, Typography } from '@mui/material' + +import { REDIRECT_EMAIL_MSG } from '../constants' + +const Email = () => { + return ( + + + Thank you for registering! + + + {REDIRECT_EMAIL_MSG} + + + ) +} + +export default Email diff --git a/src/pages/SignWithCosmos.tsx b/src/pages/SignWithCosmos.tsx index 144ce76..46a1db8 100644 --- a/src/pages/SignWithCosmos.tsx +++ b/src/pages/SignWithCosmos.tsx @@ -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 { diff --git a/src/pages/SignWithNitroKey.tsx b/src/pages/SignWithNitroKey.tsx index 3580ee3..51b9754 100644 --- a/src/pages/SignWithNitroKey.tsx +++ b/src/pages/SignWithNitroKey.tsx @@ -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, }, }); } diff --git a/src/pages/TermsAndConditions.tsx b/src/pages/TermsAndConditions.tsx index 25a9879..a0f8112 100644 --- a/src/pages/TermsAndConditions.tsx +++ b/src/pages/TermsAndConditions.tsx @@ -9,7 +9,7 @@ const TermsAndConditions = () => { const navigate = useNavigate(); const handleAccept = () => { - navigate('/connect-wallet'); + navigate('/verify-email'); }; return ( diff --git a/src/pages/Thanks.tsx b/src/pages/Thanks.tsx new file mode 100644 index 0000000..c8a18f0 --- /dev/null +++ b/src/pages/Thanks.tsx @@ -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(); + + 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 ( + + + {err ? err : "Loading..."} + + + ); +}; + +export default Thanks; diff --git a/src/pages/UserVerification.tsx b/src/pages/UserVerification.tsx index 659824f..31ffac7 100644 --- a/src/pages/UserVerification.tsx +++ b/src/pages/UserVerification.tsx @@ -50,7 +50,7 @@ const UserVerification = () => { message, cosmosAddress, receivedEthSig, - kycIdHash, + subscriberIdHash: kycIdHash, }}) } }, [applicationSubmitted, kycId, navigate, cosmosAddress, message, receivedEthSig]); diff --git a/src/pages/VerifyEmail.tsx b/src/pages/VerifyEmail.tsx new file mode 100644 index 0000000..4c8f053 --- /dev/null +++ b/src/pages/VerifyEmail.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +const VerifyEmail = () => { + return ( +
+ +
+ ) +} + +export default VerifyEmail diff --git a/yarn.lock b/yarn.lock index 6cdf7e1..7733dc9 100644 --- a/yarn.lock +++ b/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"