Setup and the User Interface
In the first part, you are going to setup the development environment for developing the dApp. It's a basic next.js setup with some dependencies.
To get you started there is a github repo with stubs and relevant file
structures. Go ahead and clone it and checkout the tutorial
branch that only
contains the stubs.
git clone git@github.com:niklasp/polkadot-js-tokengated-website.git
cd polkadot-js-tokengated-website
git checkout tutorial
Then install all dependencies
yarn
To start a development server run
yarn dev
You will notice some error outputs in the console when running the server, like:
[next-auth][warn][NEXTAUTH_URL]
https://next-auth.js.org/warnings#nextauth_url
[next-auth][warn][NO_SECRET]
https://next-auth.js.org/warnings#no_secret
That is because the repo uses environment variables for different settings. The
repo contains a /.env.local.example
file. Go ahead and rename that file to
.env.local
and change the variables if you want. Now the error message should
be gone
In a production environment, the security of your dApp / authentication relies
on NEXTAUTH_SECRET
being long and secure and not shared. The NEXTAUTH_URL
should also point to something else in production.
Great. Now that the dependencies and development environment are setup, you can build the UI in the next step.
User Interface
We are going to build two main UI components in this demo:
- The Account Select Component, which will allow a user to select the wallet
they want to use for the dApp. It will use
accounts
data from the hook we will write in the - The Login Button Component, which will handle login state, call login functionality, and display feedback on error and loading to the user.
Account Select
The Account Select component will be a simple dropdown that lets the user select
the account they want to use for the website. It needs to interact with the
browser wallet extension (we use the polkadot.js extension in this tutorial), in
order to receive all accounts
. It also stores the index of the currently
selected account.
First, have a look at in components/account-select.tsx
. Think about how you
would do it yourself, first before proceeding. The accounts
are given, try to
write a <select>
with <option>
and an onChange
function, that calls the
setActingAccountIdx
function.
Next, let's write the component.
export default function AccountSelector() {
const { accounts, actingAccount, setActingAccountIdx } =
usePolkadotExtensionWithContext();
return (
<select
onChange={(event) => {
const accountIdx = accounts
? accounts.findIndex(
(account) => account.address === event.target.value.address
)
: 0;
setActingAccountIdx(accountIdx);
}}
value={actingAccount?.address}
>
{accounts?.map((acc) => (
<option key={acc.address} value={acc.address}>
{acc.meta?.name} - {acc.address}
</option>
))}
</select>
);
}
The most relevant part is the onChange
function that updates the
actingAccountIdx
state variable, when the user changes the selected element
from the dropdown.
Now the only thing is missing is the accounts
. In the next part of the
tutorial you will code a react ContextProvider that will provide all accounts
(as well as other app state variables) to the AccountSelect
component. You
will afterwards update this component but leave it for now and look at the next
UI component you need.
Login Button
The LoginButton
component is more complex than the AccountSelect
but it will
be breaken down into parts in this paragraph. Have a look at the
components/login-button.tsx
file.
You will find 2 TODO
s
- to write the
handleLogin
function. - to write the component JSX
The JSX is the easier part, you can try yourself before proceeding. There should be 2 nested branches that will change the UI:
- If
accounts
is defined and has a length - If
session
is defined and not null
We will look at the handle login
together below.
import { useState } from "react";
import { useSession, signIn, signOut, getCsrfToken } from "next-auth/react";
import { useRouter } from "next/router";
import Link from "next/link";
import { usePolkadotExtensionWithContext } from "@/context/polkadotExtensionContext";
import AccountSelect from "./account-select";
import styles from "@/styles/Home.module.css";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export default function LoginButton() {
const router = useRouter();
const [error, setError] = (useState < string) | (undefined > undefined);
const [isLoading, setIsLoading] = useState(false);
// TODO write ContextProvider
const { accounts, actingAccount, injector } =
usePolkadotExtensionWithContext();
const handleLogin = async () => {
// TODO login functionality will come here
};
// a next-auth hook that will return the session data, i.e. the authenticated user
// used to handle component state
const { data: session } = useSession();
return (
<>
{accounts && accounts.length > 0 ? (
<>
<div className={styles.cardWrap}>
<div className={styles.dropDownWrap}>
{!session && <AccountSelect />}
</div>
{session ? (
<>
<Link href="/protected-api" className={styles.card}>
<h2 className={inter.className}>
🎉 View Tokengated Route <span>-></span>
</h2>
<p className={inter.className}>
You passed the tokengate {session.user?.name}. You can now
view the protected route.
</p>
</Link>
<div
role="button"
onClick={() => signOut()}
className={styles.card}
>
<h2 className={inter.className}>
Sign Out <span>-></span>
</h2>
<p className={inter.className}>
Click here to sign out your account {session.user?.name}.
</p>
</div>
</>
) : (
<div
role="button"
onClick={() => handleLogin()}
className={styles.card}
>
<h2 className={inter.className}>
🔑 Let me in <span>-></span>
</h2>
<p className={inter.className}>
Click here to sign in with your selected account and check if
you can view the tokengated content. <br></br>
You need > 1 KSM free balance.
</p>
</div>
)}
</div>
{isLoading ? (
<>Signing In ...</>
) : (
<span className={styles.error}> {error} </span>
)}
</>
) : (
<div className={styles.walletInfo}>
<p>
Please{" "}
<a
className={styles.colorA}
href="https://polkadot.js.org/extension/"
>
install a polkadot wallet browser extension
</a>{" "}
to test this dApp.
</p>
<p>
If you have already installed it allow this application to access
it.
</p>
</div>
)}
</>
);
}
Until now, the file basically contains the imports we will need plus some jsx
logic. Take some minutes to understand the above code. The outer conditional
checks wether there is data in accounts
(Line 33). If not, there is likely no
polkadot browser extension installed, or the user has not granted the dApp
access. In that case, a message with a link to download and install the
extension is displayed.
In the case of available accounts, there is another conditional, checking if the
session
variable is present (Line 39). If it is, the user is successfully
authenticated already, and we show a logout button. If the session
is not
available, the user is not authenticated and a button that calls the
handleLogin
function is displayed.
After copying the code you will have some errors. That is, like in the
AccountSelect
component, because you have not defined the
usePolkadotExtensionWithContext()
function yet. You will do that in the
next chapter. But first focus on
the handleLogin
function that is a stub.
Handle the login
The handleLogin
function is responsible for 4 important things:
- Constructing a
message
that contains relevant information about the user who is trying to authenticate, and also what they are authenticating against. - Ask the browser extension to
sign
that message by the user - Send that signature to the next server side where it can be used to check if it is really the account making the request
- Updating the UI with feedback for the user (loading, error).
Have a look at the function before
const handleLogin = async () => {
try {
setIsLoading(true);
let signature = "";
const message = {
statement:
"Sign in with polkadot extension to the example tokengated example dApp",
uri: window.location.origin,
nonce: await getCsrfToken(),
version: "1",
};
const signRaw = injector?.signer?.signRaw;
if (!!signRaw && !!actingAccount) {
// after making sure that signRaw is defined
// we can use it to sign our message
const data = await signRaw({
address: actingAccount.address,
data: JSON.stringify(message),
type: "bytes",
});
signature = data.signature;
}
// will return a promise https://next-auth.js.org/getting-started/client#using-the-redirect-false-option
const result = await signIn("credentials", {
redirect: false,
callbackUrl: "/protected-api",
message: JSON.stringify(message),
name: actingAccount?.meta?.name,
signature,
address: actingAccount?.address,
});
// take the user to the protected page if they are allowed
if (result?.url) {
router.push("/protected-api");
}
setError(result?.error);
setIsLoading(false);
} catch (error) {
setError("Cancelled Signature");
setIsLoading(false);
}
};
Message and Signature
The browser extensions can be used to sign arbitrary data with the private keys of the acting account. This signature alongside the corresponding public key can then be used to verify that it was really the user who signed the message. The message contains
- a
statement
that will be shown in the browser extension signature dialog and notify the user about what they are signing. It is one part of making sure the user signed something from our dApp and not from any other dApp - a
uri
to verify that the user interacted with the dApp at a specific uri - a
nonce
to mitigate replication attacks, i.e. if someone gets access to the signature, that signature cannot be used again to login into your dApp - and finally a
version
: the version of the dApp might change in critical ways, rendering signatures invalid. We are not using it in our dApp but it is a good practice to have.
All those data are signed which means that with that signature, we can really
verify that the private key has used our dApp at the uri
with the specified
version.
Learn more on Polkadot's Sign and Verify
That was a lot. Take a moment to go over the material in this chapter again and
make sure you understood things. The following two chapters will teach you about
how to populate the accounts
we used in several places already and how
next-auth is used to create the actual token gated authentication.