In my last post, I wrote about the difficulty I was having implementing third-party tools to connect to a user's WAX wallet to log them into my website. I was able to use an older tool called Anchor-Link to invoke the Anchor app to handle the login process for the user. This worked successfully, but I had two problems with it. First, I was having trouble showing different pages to users who were logged in and users who were not logged in. I used some convoluted routing and some session variables on my website to store the logged in account. The second problem I had was that I really wanted to be able to support more login providers. At the very least, I want my app to support Anchor, Cloud Wallet, and Wombat.
After writing my last post, I took a little break from developing the web side of the game to work on the proof-of-concept Console Application because I was between seasons. My game will run four 3-month-long seasons each year, and so I happened to be in the middle of a seasonal transition. So I took some time out to build in some additional features in my Console Application that would allow me to set up new seasons and close out old seasons. But now I'm back to working on the website again.
I did find a sample website template that implemented @Wharfkit to create a website that utilizes the wharfkit modules to handle sessions and transactions. The Wharfkit module supports multiple wallet providers, which makes it the perfect tool for me to build a website that can be utilized by all players, regardless of what type of account they use to access their wallet.
Instead of using the template provided by Wharfkit, I decided to create a new Vite project myself by scaffolding the default Vite application using npm create vite@latest, and then watching several YouTube Vite tutorials and implementing certain components piecemeal, and finally trying to import Wharfkit into the whole mess. This approach was a mistake because it scaffolded out a large project that I didn't fully understand. Even so, I decided to start with that Frankenstein project to build my website.
I had a lot of trouble getting started, mostly because of the approach I used. As I mentioned in my last post, I am very new to Node.js development, which is required to access Wharfkit's plugin. But the Frankenstein project I ended up with was utilizing not only Node.js, but also Vite, React, and TypeScript, and I had absolutely no understanding of how any of these tools worked. Although Vite, React, and TypeScript were the primary tools, the site also relied on NextUI and Tailwind to style the website. All of these tools are powerful, modern web development tools, and I can see why people would want to use them. But when you have never used them before and you suddenly have to take apart and rebuild a website that is extensively utilizing these tools, it can be a bit overwhelming. But, little by little, I am getting an understanding of either how these tools work, or how to replace them with tools I am more familiar with (Like Bootstrap for the UI). I may be butchering the clean design of the original template, but all that matters to me at this point is getting a functional website.
And that's what I've finally managed to do.
Just to be clear, when I say I made a functional website, all I'm talking about so far is creating a website that users can log in to using their WAX wallet provider without my application having to handle the authentication transaction itself. I can also restrict certain pages to only be available to logged in users.
I'm going to walk through some of the code I used to build the site. Please note that this is not a comprehensive collection of all my code, but only the parts I deem relevant. Which pains me a little, because I often search the web for information on how to do things, and I get frustrated sometimes when people only provide part of the code needed to accomplish a task. But this blog post isn't intended to be a tutorial on how to build a website using Vite, React, TypeScript, and Wharfkit. It's more of a journal where I keep notes on only the most relevant parts. I'm building a commercial application I intend to make money off of, so my repository is private. But I hope that the limited code I share below is at least somewhat helpful to others who might be wanting to use Wharfkit to develop a WAX-integrated website.
Let's start with logging in using one of the wallet providers and Wharfkit. The code below represents the top navigation menu for the site, most notably a login link that switches to a button showing the logged-in user's account name if the user is logged in. Please remember that this site is very much under development, and the code below is not at all optimized for performance or quality. I'm using a combination of React components and Bootstrap, which is not recommended, but I'm just doing this for now because I can't get some of the React components to display correctly, and I'm more familiar with Bootstrap. So where I run into problems with React, I replace it with Bootstrap for now.
NavMenu.tsx
import { useEffect, Dispatch, SetStateAction, useState } from "react";
import {Button, DropdownItem, DropdownMenu, DropdownTrigger, Dropdown } from "@nextui-org/react";
import { Link } from "react-router-dom";
import { Session } from '@wharfkit/session'
import { DynamicRoutes } from "../router/routes";
import { sessionKit } from "../App";
import './navMenu.css';
export const NavMenu = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [session, setSession]: [Session | undefined, Dispatch<SetStateAction<Session | undefined>>] = useState<Session | undefined>(undefined);
useEffect(() => {
sessionKit.restore().then((session: Session | undefined) => {
if (session) {
setSession(session);
}
})
}, []);
const login = async () => {
const response = await sessionKit.login()
if (response.session) {
setSession(response.session);
}
window.location.reload();
};
const logout = async () => {
console.log('logout');
sessionKit.logout()
setSession(undefined);
window.location.reload();
}
return (
<nav>
<div className="flex items-center justify-between border-b border-gray-400" key={'navmaindiv'}>
<Link to={"/"} style={{ width: "auto", marginLeft: "1em" }} key={'image_logo'} className="sm:flex gap-4">
<img
width={60}
height={60}
src={"/img/logo.png"}
alt={import.meta.env.VITE_APP_NAME}
title="Home"
/>
</Link>
<ul className="lg:flex justify-center flex-grow">
{
DynamicRoutes.map((route) => {
if (!route.showInMenu) return null;
if (route.isPrivate && !session) return null;
return (
<li key={'top-menu-' + route.title}>
<Link to={route.path} aria-label={`go to ${route.title}`} >
<span>{route.title}</span>
</Link>
</li>
)
})
}
</ul>
<div className="lg:flex justify-end">
{
!session
? <p onClick={login} style={{cursor: "pointer"}}>Login</p>
: <Dropdown aria-label="user-menu" style={{border: "1px solid #000000", backgroundColor: "rgba(255,255,255,1)"}}>
<DropdownTrigger aria-label="access-user-menu">
<Button aria-label="User options menu" className="btn btn-danger" color="danger">
<div className="keepTogether">
<svg className="w-[15px] h-[15px] text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 8" style={{display: "inline", marginTop: "0.5em"}}>
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="m1 1 5.326 5.7a.909.909 0 0 0 1.348 0L13 1" />
</svg> {String(session.actor)}</div>
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="user-menu-options">
<DropdownItem aria-label="logout" onPress={logout} ><a href="#" className="text-danger">Log out</a></DropdownItem>
</DropdownMenu>
</Dropdown>
}
</div>
</div>
</nav>
)
};
In the code above, our navigation menu has three sections. The first is a <Link> to the site's logo. The next is a <UL> of pages stored as an array of JSON objects that we define elsewhere. Each item in the array has a "showInMenu" boolean property. If set to false, the link to the item is not displayed in our navigation menu. Each item also has an "isPrivate" boolean property. If set to true, the item is only included in the top navigation menu if the user has logged in and a session has been defined. This session information comes from our App.tsx file, which I will show below. The third section of the navigation bar is a <DIV> that displays a Login button if the user is not logged in, or displays a dropdown menu that shows the current user's account name and, when clicked, displays a Log Out button.
Next, let's look at our App.tsx file, which is the main webpage shell for our site. This page will provide the Wharfkit integrations that allow the user to log in and out of the site.
App.tsx
import { Suspense } from 'react'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import './App.css'
import { DynamicRoutes } from './router/routes'
import { HomePage } from './pages/'
import { ErrorPage } from './pages/errorPage'
import { MainPage } from './pages/HomePage/main'
import { chainId, ualRpc } from './data/blockchain.tsx'
// Wharfkit setup
import { SessionKit } from '@wharfkit/session'
import { WalletPluginAnchor } from '@wharfkit/wallet-plugin-anchor'
import { WalletPluginCloudWallet } from '@wharfkit/wallet-plugin-cloudwallet'
import { WalletPluginWombat } from '@wharfkit/wallet-plugin-wombat'
import WebRenderer from '@wharfkit/web-renderer'
const isMainNet=(import.meta.env.VITE_BLOCKCHAIN==='mainnet');
const chains = [
{
id: chainId,
url: ualRpc,
}
];
const walletPlugins = [];
walletPlugins.push(new WalletPluginAnchor());
if (isMainNet) {
walletPlugins.push(new WalletPluginCloudWallet());
walletPlugins.push(new WalletPluginWombat());
}
export const sessionKit = new SessionKit({
appName: import.meta.env.VITE_APP_NAME,
chains,
ui: new WebRenderer(),
walletPlugins,
})
// end Wharfkit setup
const router = createBrowserRouter([
{
path: "/",
element: <HomePage />,
errorElement: <ErrorPage />,
children: [
{
path: "/",
element: <MainPage />,
},
...DynamicRoutes.map((route) => {
return {
path: route.path,
element: <route.component />,
}
})
]
},
]);
function App() {
return (
<div>
<Suspense fallback={<>Loading...</>}>
<RouterProvider router={router} />
</Suspense>
</div>
)
}
export default App
Again, forgive the mess, this is still a work in progress. But you can see in the Wharfkit Setup section of the page that we can easily import the required components to run Wharfkit (SessionKit and WebRenderer) as well as the specific wallet plugins we want to support in our app. I have another file where I set the chainId and ualRpc parameters depending on whether my app is configured to use the mainnet or the testnet. I'm also checking that configuration on this page to determine which plugins I should support with Wharfkit. When I am using the testnet, the Cloud Wallet and Wombat logins won't work, so I leave them off and only support Anchor. But when I am using the mainnet, all three wallet providers will be supported. The SessionKit created here is exported so it can be accessed by the NavMenu.tsx file, which calls the login and logout functions.
The final element I will discuss in this post is restricting access to certain pages based on whether the user is logged in or not. This turned out to be relatively easy to implement, and was something that I picked up either out of the default Vite template or one of the other base projects I pulled in to my starting site...I certainly didn't come up with any of this myself. I've already discussed how to keep the links to the restricted pages out of the navigation menu if the user is not logged in, but what happens if they are on a restricted page when they log out, or they type in the URL to a restricted page without clicking on the navigation link?
To handle the first situation, where the user is already on the restricted page when they click "Log out," I have added a "window.location.reload();" line to the logout function in NavMenu.tsx to force the page to reload when the user logs out. Not the most elegant or React-friendly solution, but it works for my purposes. To actually prevent the user from navigating to a specific page if their session does not exist, I add a <Protected> element around the page, like this:
RestrictedPage.tsx
import { Container } from 'react-bootstrap'
import { Protected } from './protected'
export function RestrictedPage() {
return (
<Protected>
<Container className="my-4">
<h1>This page is visible only to logged-in users</h1>
</Container>
</Protected>
)
};
Protected.tsx
import { Navigate } from "react-router-dom";
import { useState } from 'react'
type ProtectedProps = {
children: any
};
const Protected = ({children}: ProtectedProps) => {
let isLoggedIn = false;
useState(() => {
const ws = localStorage.getItem("wharf--sessions");
const wsj = JSON.parse(ws || '[{}]');
if (wsj[0]['actor']) {
isLoggedIn=true;
}
else {
isLoggedIn=false;
}
})
if (!isLoggedIn) {
console.log('User is not logged in...redirecting to home page')
return <Navigate to="/" replace />;
}
return children;
};
export { Protected };
I've had a little trouble checking the login status using Wharfkit in some of my page elements, so for now, I am just looking at localStorage, where Wharfkit stores its session data. I look to see if there is a "wharf--sessions" object in local storage that has an "actor" property. If it's not found, the user is not logged in, otherwise, the user is logged in. If the user is not logged in, I use a React <Navigate> element to redirect the browser to the home page of the website. And, viola, the user is not able to access restricted pages until/unless they have logged into the site. Eventually, I plan on using the session data from App.tsx to determine the user's login status, but for now, localStorage is working fine.
Obviously, I have a lot of work left to do. But for now, I actually have a functional application that can integrate with Wharfkit to allow users to log into and out of the website using a WAX wallet provider.