·7 min read

Preference Storage for DApps using Metamask with Next.js

Burak YılmazBurak YılmazSite Reliability Engineer @Upstash

Web3 applications such as DAOs and DApps are getting more and more popular. By the premises of Web3, these platforms are supposed to provide a more personal and customized experience for their users while keeping their identities private from others or even unknown to themselves.

In this project, we explore how we can enhance the user experience for such cases.

Authentication / Identity Check through Wallets

For many applications, blockchain wallets like Metamask are used to manage crypto assets such as ETH. Such wallets let users create accounts on different chains. These accounts are created with private keys; however, each account has a public address representing it.

Since public keys can be shown by anyone, but transactions related to them can only be managed by the owner, wallet connections provide a solid authentication mechanism for such applications.

The Project

In this project, we will implement a preference storage system using Next.js, where users can customize the interface provided to them. Meaning, whenever they come back to that site, they will see the UI as they intended.

Even more powerful ideas could involve normalizing user interfaces across companies' sub & cross-platforms. This way, whenever a user navigates to a certain site supporting the preferences they set on other sites, they will have a similar experience.

Also, this project can be modified to change the number of parameters for customization, allowing you to mold it into whatever structure you have in mind.

For this sample project, there are only two parameters, namely:

  • Theme of the website
  • Greeting name for the user

Getting Started

Configure Upstash Redis Environmental Variables

  • Create a free Redis database at Upstash
  • Copy the .env.local.example file to .env.local (which will be ignored by Git):
  • UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN can be found at the database details page in Upstash Console.

Dependencies

Install dependencies: npm i @upstash/redis @metamask/detect-provider @mui/material @emotion/react @emotion/styled

Create api/store.js file

Configure Redis SDK and export the handler function.

import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()

export default async function handler(req, res) {
  const accountID = await JSON.parse(req.body).accountID
  const body = req.body
  const setResult = await redis.set(accountID, body);
  res.status(200).json({ result: setResult })
}
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()

export default async function handler(req, res) {
  const accountID = await JSON.parse(req.body).accountID
  const body = req.body
  const setResult = await redis.set(accountID, body);
  res.status(200).json({ result: setResult })
}

Create api/[accountAddress].js file

Similarly, configure Redis SDK and export the handler.

import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()

export default async function handler(req, res) {
    const accountID = req.query.accountAddress
    const getResult = await redis.get(accountID)
    res.status(200).json({ result: getResult })
}
import { Redis } from "@upstash/redis"
const redis = Redis.fromEnv()

export default async function handler(req, res) {
    const accountID = req.query.accountAddress
    const getResult = await redis.get(accountID)
    res.status(200).json({ result: getResult })
}

Configure index.js file

Set your variables for the project

const [accountAddress, setAccountAddress] = useState(null)
const [themePreference, setThemePreference] = useState("light")
const [greetingMessage, setGreetingMessage] = useState("Anonymous Person")
const [userSettings, setUserSettings] = useState(null)

// Check if connection already established. If so, fetch the data.
useEffect(() => {
    checkConnection()
    getPreferences()
}, [accountAddress])
const [accountAddress, setAccountAddress] = useState(null)
const [themePreference, setThemePreference] = useState("light")
const [greetingMessage, setGreetingMessage] = useState("Anonymous Person")
const [userSettings, setUserSettings] = useState(null)

// Check if connection already established. If so, fetch the data.
useEffect(() => {
    checkConnection()
    getPreferences()
}, [accountAddress])

Connecting Metamask

async function connect() {
  const provider = await detectEthereumProvider();
  console.log("provider:", provider);
 
  if (provider) {
    console.log("Ethereum successfully detected!");
    provider
      .request({ method: "eth_requestAccounts" })
      .then((accounts) => {
        if (!accountAddress) {
          setAccountAddress(accounts[0]);
        }
      })
      .catch((err) => console.log(err));
    console.log("window.ethereum:", window.ethereum);
    getPreferences();
  } else {
    alert("Please install MetaMask!");
  }
}
async function connect() {
  const provider = await detectEthereumProvider();
  console.log("provider:", provider);
 
  if (provider) {
    console.log("Ethereum successfully detected!");
    provider
      .request({ method: "eth_requestAccounts" })
      .then((accounts) => {
        if (!accountAddress) {
          setAccountAddress(accounts[0]);
        }
      })
      .catch((err) => console.log(err));
    console.log("window.ethereum:", window.ethereum);
    getPreferences();
  } else {
    alert("Please install MetaMask!");
  }
}

Checking Connection when refreshed or navigated after some time

async function checkConnection() {
  const provider = await detectEthereumProvider();
  if (provider) {
    provider
      .request({ method: "eth_accounts" })
      .then((accounts) => {
        setAccountAddress(accounts[0]);
      })
      .catch(console.log);
  } else {
    console.log("Not connected, window.ethereum not found");
  }
}
async function checkConnection() {
  const provider = await detectEthereumProvider();
  if (provider) {
    provider
      .request({ method: "eth_accounts" })
      .then((accounts) => {
        setAccountAddress(accounts[0]);
      })
      .catch(console.log);
  } else {
    console.log("Not connected, window.ethereum not found");
  }
}

To set preferences

async function setPreferences(themePreference, greetingMessage) {
  if (accountAddress) {
    const res = await fetch(`/api/store`, {
      method: "POST",
      body: JSON.stringify({
        accountID: accountAddress,
        themePreference: themePreference,
        greetingMessage: greetingMessage,
      }),
    });
    const data = await res.json();
  } else {
    alert("No account address detected");
  }
}
async function setPreferences(themePreference, greetingMessage) {
  if (accountAddress) {
    const res = await fetch(`/api/store`, {
      method: "POST",
      body: JSON.stringify({
        accountID: accountAddress,
        themePreference: themePreference,
        greetingMessage: greetingMessage,
      }),
    });
    const data = await res.json();
  } else {
    alert("No account address detected");
  }
}

To get preferences

async function getPreferences(e) {
  if (accountAddress) {
    console.log("Fetching user preferences...");
    const res = await fetch(`/api/${accountAddress}`, { method: "GET" });
    const data = await res.json();
 
    setUserSettings(data.result);
    if (data.result) {
      setThemePreference(data.result.themePreference);
      setGreetingMessage(data.result.greetingMessage);
    }
  } else {
    console.log("No account connected yet!");
  }
}
async function getPreferences(e) {
  if (accountAddress) {
    console.log("Fetching user preferences...");
    const res = await fetch(`/api/${accountAddress}`, { method: "GET" });
    const data = await res.json();
 
    setUserSettings(data.result);
    if (data.result) {
      setThemePreference(data.result.themePreference);
      setGreetingMessage(data.result.greetingMessage);
    }
  } else {
    console.log("No account connected yet!");
  }
}

Change state of Variables

Bind the following functions to input fields/buttons etc.

async function handleDarkMode(e) {
  console.log("themePreference:", themePreference);
  const newPreference = themePreference == "light" ? "dark" : "light";
  setThemePreference(newPreference);
  await setPreferences(newPreference, greetingMessage);
  await getPreferences();
}
 
async function takeGreetingMessage(e) {
  // submit with enter/return key
  if (e.keyCode == 13) {
    const message = e.target.value;
    setGreetingMessage(message);
    console.log(message);
    await setPreferences(themePreference, message);
    await getPreferences();
    e.target.value = "";
  }
}
async function handleDarkMode(e) {
  console.log("themePreference:", themePreference);
  const newPreference = themePreference == "light" ? "dark" : "light";
  setThemePreference(newPreference);
  await setPreferences(newPreference, greetingMessage);
  await getPreferences();
}
 
async function takeGreetingMessage(e) {
  // submit with enter/return key
  if (e.keyCode == 13) {
    const message = e.target.value;
    setGreetingMessage(message);
    console.log(message);
    await setPreferences(themePreference, message);
    await getPreferences();
    e.target.value = "";
  }
}

Create a simple UI utilizing the above functions

Make sure you imported relevant Material-UI dependencies.

return (
  <div className={styles.container}>
    <h2>Web3 Preferences Holder</h2>
    <Button variant="contained" onClick={connect}>
      Connect Metamask
    </Button>
    <p>Lets you keep user preferences on cross-websites</p>
    <br />
    <p>For example, take a greeter message from user.</p>
    <TextField
      label="Call me..."
      variant="outlined"
      size="small"
      onKeyDown={takeGreetingMessage}
    />
    <br />
    <br />
 
    <Button
      onClick={handleDarkMode}
      variant="contained"
      size="small"
      style={{ backgroundColor: "#3D3B3B" }}
    >
      {" "}
      Switch Dark Mode{" "}
    </Button>
 
    <p>Sample Component/Page:</p>
    <Showcase userSettings={userSettings} />
  </div>
);
return (
  <div className={styles.container}>
    <h2>Web3 Preferences Holder</h2>
    <Button variant="contained" onClick={connect}>
      Connect Metamask
    </Button>
    <p>Lets you keep user preferences on cross-websites</p>
    <br />
    <p>For example, take a greeter message from user.</p>
    <TextField
      label="Call me..."
      variant="outlined"
      size="small"
      onKeyDown={takeGreetingMessage}
    />
    <br />
    <br />
 
    <Button
      onClick={handleDarkMode}
      variant="contained"
      size="small"
      style={{ backgroundColor: "#3D3B3B" }}
    >
      {" "}
      Switch Dark Mode{" "}
    </Button>
 
    <p>Sample Component/Page:</p>
    <Showcase userSettings={userSettings} />
  </div>
);

Create sampleComponent.jsx in a directory components

This is the main component representing your web interface, application, etc. This component will visualize how the system works and the main goal it provides. Here, parse the parameters however you like and create your sample interface.

In this instance, the project is configured as such:

import React, { useState } from "react";
 
import { grey, orange } from "@mui/material/colors";
import { createTheme, ThemeProvider } from "@mui/material/styles";
 
const lightTheme = createTheme({
  palette: {
    primary: {
      main: grey[400],
    },
  },
});
 
const darkTheme = createTheme({
  palette: {
    primary: {
      main: orange[400],
    },
  },
});
 
export default function Showcase(parameters) {
  const userSettings = parameters.userSettings;
  const [theme, setTheme] = useState("light");
  const [greetingMessage, setGreetingMessage] = useState("Anonymous Person");
 
  const items = [];
  if (userSettings) {
    const obj = userSettings[0];
 
    for (const key in userSettings) {
      items.push(
        <li key={key}>
          {" "}
          {key}: {userSettings[key]}{" "}
        </li>,
      );
    }
 
    if (userSettings["themePreference"] != theme) {
      setTheme(userSettings["themePreference"] == "light" ? "light" : "dark");
    }
 
    if (
      !greetingMessage ||
      userSettings["greetingMessage"] != greetingMessage
    ) {
      if (userSettings["greetingMessage"]) {
        setGreetingMessage(userSettings["greetingMessage"]);
      } else {
        setGreetingMessage("not the same message");
      }
    }
  }
 
  return (
    <div>
      <div
        style={{
          padding: 10,
          margin: 10,
          backgroundColor: theme == "light" ? "grey" : "orange",
          border: "solid",
          borderWidth: "30px",
          borderColor: theme == "light" ? "#B2B2B2" : "black",
        }}
      >
        <ThemeProvider theme={theme == "light" ? lightTheme : darkTheme}>
          <h2>Hi, {greetingMessage}!</h2>
          <p>User and their preferences:</p>
          {items}
        </ThemeProvider>
      </div>
    </div>
  );
}
import React, { useState } from "react";
 
import { grey, orange } from "@mui/material/colors";
import { createTheme, ThemeProvider } from "@mui/material/styles";
 
const lightTheme = createTheme({
  palette: {
    primary: {
      main: grey[400],
    },
  },
});
 
const darkTheme = createTheme({
  palette: {
    primary: {
      main: orange[400],
    },
  },
});
 
export default function Showcase(parameters) {
  const userSettings = parameters.userSettings;
  const [theme, setTheme] = useState("light");
  const [greetingMessage, setGreetingMessage] = useState("Anonymous Person");
 
  const items = [];
  if (userSettings) {
    const obj = userSettings[0];
 
    for (const key in userSettings) {
      items.push(
        <li key={key}>
          {" "}
          {key}: {userSettings[key]}{" "}
        </li>,
      );
    }
 
    if (userSettings["themePreference"] != theme) {
      setTheme(userSettings["themePreference"] == "light" ? "light" : "dark");
    }
 
    if (
      !greetingMessage ||
      userSettings["greetingMessage"] != greetingMessage
    ) {
      if (userSettings["greetingMessage"]) {
        setGreetingMessage(userSettings["greetingMessage"]);
      } else {
        setGreetingMessage("not the same message");
      }
    }
  }
 
  return (
    <div>
      <div
        style={{
          padding: 10,
          margin: 10,
          backgroundColor: theme == "light" ? "grey" : "orange",
          border: "solid",
          borderWidth: "30px",
          borderColor: theme == "light" ? "#B2B2B2" : "black",
        }}
      >
        <ThemeProvider theme={theme == "light" ? lightTheme : darkTheme}>
          <h2>Hi, {greetingMessage}!</h2>
          <p>User and their preferences:</p>
          {items}
        </ThemeProvider>
      </div>
    </div>
  );
}

Workflow

User not connected via Metamask yet. Default Template.

web3-before-connection

User connected via Metamask for the first time. Default Template.

web3-initial-preferences

  • User enters a name that they want to see when greeted to platform.
  • User selects between themes, namely light and dark.

User preferences are set up. From now on, same interface can be shown in any platform supporting the preference interface provided. User-customized template.

web3-changed-preferences

The component shows the preferences set by the user in text format; you can parse them however you like.

As you can see in this simple example, you can parametrize any component that you show to the user. You can always add to this project by improving the design and increasing the number of parameters; however, the fundamental logic stays the same.

Final Words

To see a demo of the project, check here.

To see the finished project, go to Github Repo of the project.

There, you will see a quick deploy button for Vercel deployment; letting you deploy the project quickly, creating an Upstash Redis integration automatically.

We are looking forward to hearing your ideas and thoughts. You can reach us via twitter or discord.