Skip to content

Instantly share code, notes, and snippets.

@Olanetsoft
Last active May 14, 2024 14:51
Show Gist options
  • Save Olanetsoft/6ec8fd65598a4836313b46955f5a9a42 to your computer and use it in GitHub Desktop.
Save Olanetsoft/6ec8fd65598a4836313b46955f5a9a42 to your computer and use it in GitHub Desktop.
Decentralized Identity – Build a Profile with Next.js, Ethereum & Ceramic Network

Decentralized Identity – Build a Profile with Next.js, Ethereum & Ceramic Network

In this workshop, you will learn about how to build a decentralized identity profile with Ethereum on Ceramic Networks.

Prerequisites

  • To go through this tutorial, you'll need some experience with JavaScript and React.js. Experience with Next.js isn't a requirement, but it's nice to have.

  • Make sure to have Node.js or npm installed on your computer. If you don't, click here.

Also, it'll be very useful to have a basic understanding of blockchain technology and Web3 concepts.

Project Setup and Installation

Navigate to the terminal and cd into any directory of your choice. Then run the following commands:

mkdir decentralized-identity-project
cd decentralized-identity-project

npx create-next-app@latest .

Accept the following options:

0b46fd0f-d47a-4533-9450-a79007205efe

Install the react-hot-toast, @self.id/react and @self.id/web packages using the code snippet below:

npm install react-hot-toast @self.id/web @self.id/react

Next, start the app using the following command:

npm run dev

You should have something similar to what is shown below: the default boilerplate layout for Next.js 13.

799cfc73-78b3-49f9-8b72-a407813f7d9c

Install TailwindCSS in Next.js

In this section, you will set up Tailwind CSS in a Next.js project. Install tailwindcss and its peer dependencies via npm, and then run the init command to generate both tailwind.config.js and postcss.config.js.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Navigate to the tailwind.config.js file, and add the paths to your template files with the following code snippet.

/** @type {import('tailwindcss').Config} */

module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
 
    // Or if using `src` directory:
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Delete all the CSS styles inside globals.css . Add the @tailwind directives for each of Tailwind’s layers to your globals.css file.

@tailwind base;
@tailwind components;
@tailwind utilities;

Configure the Provider Component

The Provider component must be placed at the top of the application tree to use the hooks detailed below. You can use it to supply an initial state as well as a specific configuration for the Self.ID clients and queries.

Update the _app.js file under the pages folder with the following code snippet:

// Import the Provider component from the "@self.id/react" library.
import { Provider } from "@self.id/react";

// Import the "globals.css" file from the "@/styles" directory.
import "@/styles/globals.css";

// Define the App component as a default export.
export default function App({ Component, pageProps }) {
    
  // Render the Provider component, which provides authentication and authorization functionality to the application.
  // Pass a client prop to the Provider component, which configures the Ceramic testnet with the "testnet-clay" value.
  // Render the Component with its props inside the Provider component, which allows the application to access the authentication and authorization context.
    
  return (
    <Provider client={{ ceramic: "testnet-clay" }}>
      <Component {...pageProps} />
    </Provider>
  );
}

Configure Provider

In the code snippet above, we:

  • Imported a context provider component and global CSS styles and then defined an App component that wraps the entire application with the context provider.

  • Configured the context provider with a Ceramic testnet client, which allows the application to access authentication and authorization functionality.

  • Finally, the Component is rendered with its props inside the context provider, allowing the application to access the authentication and authorization context.

Build the Layout

Next, navigate to the index.js file under the pages folder and update it with the following code:

// Import the Head component from the "next/head" module.
import Head from "next/head";

// Import the useViewerConnection and useViewerRecord hooks from the "@self.id/react" library.
import { useViewerConnection, useViewerRecord } from "@self.id/react";

// Import the EthereumAuthProvider component from the "@self.id/web" library.
import { EthereumAuthProvider } from "@self.id/web";

// Import the toast and Toaster components from the "react-hot-toast" library.
import { Toaster, toast } from "react-hot-toast";

// Import the useState hook from the "react" module.
import { useEffect, useState } from "react";

export default function Home() {

  return (
    <>
      <Head>
        <title>Decentralized Identity Demo</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className="min-h-screen bg-gray-100">
        <nav className="bg-white shadow">
          <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
            <div className="flex justify-between h-16">
              <div className="flex">
                <div className="flex-shrink-0 flex items-center">
                  <h3 className="text-2xl font-bold text-gray-900">
                    Decentralized Identity
                  </h3>
                </div>
              </div>
              <div className="flex items-center">
                <button
                  className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
                >
                  Connect Wallet
                </button>
              </div>
            </div>
          </div>
        </nav>

        <main className="py-10">
          <div className="max-w-3xl mx-auto sm:px-6 lg:px-8">
            <div className="bg-white overflow-hidden shadow sm:rounded-lg px-4 py-5 sm:p-6">
              <div className="px-4 py-5 sm:p-6 bg-white">
                <form>
                  <div className="space-y-4">
                    <div>
                      <label
                        htmlFor="name"
                        className="block text-sm font-bold text-gray-700"
                      >
                        Name
                      </label>
                      <div className="mt-1">
                        <input
                          type="text"
                          name="name"
                          id="name"
                          className="block w-full px-4 py-3 rounded-md shadow-sm placeholder-gray-500 focus:ring-gray-500 focus:border-gray-500 border border-gray-300 focus:outline-none "
                          placeholder="John Doe"
                        />
                      </div>
                    </div>
                    <div>
                      <label
                        htmlFor="username"
                        className="block text-sm font-bold text-gray-700"
                      >
                        Username
                      </label>
                      <div className="mt-1">
                        <input
                          type="text"
                          name="username"
                          id="username"
                          className="block w-full px-4 py-3 rounded-md shadow-sm placeholder-gray-500 focus:ring-gray-500 focus:border-gray-500 border border-gray-300 focus:outline-none "
                          placeholder="johndoe"
                        />
                      </div>
                    </div>
                    <div>
                      <label
                        htmlFor="bio"
                        className="block text-sm font-bold text-gray-700"
                      >
                        Bio
                      </label>
                      <div className="mt-1">
                        <textarea
                          name="bio"
                          id="bio"
                          className="block w-full px-4 py-3 rounded-md shadow-sm placeholder-gray-500 focus:ring-gray-500 focus:border-gray-500 border border-gray-300 focus:outline-none "
                          rows="3"
                          placeholder="Tell us a little about yourself..."
                        ></textarea>
                      </div>
                    </div>
                    <div className="flex justify-end">
                      <button
                        type="submit"
                        className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
                      >
                        Update Profile
                      </button>
                    </div>
                  </div>
                </form>
              </div>
            </div>
          </div>
        </main>
      </div>
    </>
  );
}

To start the application, run the following command and navigate to localhost:3000 on your browser; you should have something similar to what is shown below:

Layout

How to Authenticate Users

In this section, you will implement user authentication to allow users to connect their wallets and interact with the application.

Update the index.js with the following code:

//...

export default function Home() {
  // Calls the useViewerConnection hook to get the connection status, connect and disconnect functions.
  const [connection, connect, disconnect] = useViewerConnection();

  // Sets up the isWindow state variable to null using useState.
  const [isWindow, setIsWindow] = useState(null);

  // Creates a new authentication provider using the ethereum account.
  async function createAuthProvider() {
    // The following assumes there is an injected `window.ethereum` provider
    const addresses = await window.ethereum.request({
      method: "eth_requestAccounts",
    });
    return new EthereumAuthProvider(window.ethereum, addresses[0]);
  }

  // Connects the user's wallet to the website using the connect function and the created authentication provider.
  async function connectAccount() {
    const authProvider = await createAuthProvider();
    connect(authProvider);
  }



  // Sets the isWindow variable to the window object if it exists.
  useEffect(() => {
    if (typeof window !== "undefined") {
      setIsWindow(window);
    }
  }, []);

  return (
    <>
      <Head>
             {/* ... */}
      </Head>
      <div className="min-h-screen bg-gray-100">
        <nav className="bg-white shadow">
          <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
            <div className="flex justify-between h-16">
              <div className="flex">
                <div className="flex-shrink-0 flex items-center">
                  <h3 className="text-2xl font-bold text-gray-900">
                    Decentralized Identity
                  </h3>
                </div>
              </div>
              <div className="flex items-center">
                {connection.status === "connected" ? (
                  <button
                    onClick={() => disconnect()}
                    className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-500 hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
                  >
                    Disconnect
                  </button>
                ) : isWindow && "ethereum" in window ? (
                  <button
                    onClick={() => connectAccount()}
                    className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
                  >
                    Connect Wallet
                  </button>
                ) : (
                  <p className="text-red-500 text-sm italic mt-2 text-center w-full">
                    An injected Ethereum provider such as{" "}
                    <a href="https://metamask.io/">MetaMask</a> is needed to
                    authenticate.
                  </p>
                )}
              </div>
            </div>
          </div>
        </nav>

        <main className="py-10">
              {/* ... */}
              
        </main>
      </div>
    </>
  );
}

In the code snippet above,

  • The useViewerConnection hook is used to set up a state variable for the user's connection status, connect and disconnect.

  • isWindow to set the initial state of the the window to avoid React hydration error

  • The useViewerRecord hook is used to retrieve the user's basic profile data.

  • The createAuthProvider function creates an EthereumAuthProvider object using the window.ethereum provider.

  • The connectAccount function calls createAuthProvider and connects to the user's account using connect(authProvider).

  • The JSX code conditionally renders a button based on the user's connection status and the availability of an ethereum provider in the window object.

  • If the user is already connected, the button will enable them to disconnect. If the user is not yet connected and an ethereum provider is available, the button will enable them to connect. But if the user is not connected and no ethereum provider is available, a message will be displayed to inform the user that an injected Ethereum provider like MetaMask is required to authenticate.

Testing out the authentication functionality, you should have something similar to what is shown below:

Connect Wallet

How to Create or Update a User Profile

In the previous section, you learned how to successfully authenticate users. Next, you will implement functionality to create and update an authenticated user with the following code snippet:

pages/index.js

//...

export default function Home() {
  // Sets up the state variables for name, username, and bio.
  const [name, setName] = useState("");
  const [username, setUsername] = useState("");
  const [bio, setBio] = useState("");
  
  // Calls the useViewerRecord hook to get the ceramic record for the user's basic profile.
  const record = useViewerRecord("basicProfile");
  
  //...
  
  // Handles the form submission and updates the user's profile on the ceramic database.
  const handleSubmit = async (event) => {
    event.preventDefault();
    if (!name || !username || !bio) {
      toast.error("Please fill out all fields");
      return;
    }

    await record.merge({
      name,
      bio,
      username,
    });
    toast.success("Profile updated");
  };
  
  
  // Render the component's UI
  
  return (
  <>
    <Head>
         {/* ... */}
    </Head>
    
     <div className="min-h-screen bg-gray-100">
        <nav className="bg-white shadow">
          {/* ... */}
        </nav>
        
           <main className="py-10">
          <div className="max-w-3xl mx-auto sm:px-6 lg:px-8">
            <div className="bg-white overflow-hidden shadow sm:rounded-lg px-4 py-5 sm:p-6">
              <div className="px-4 py-5 sm:p-6 bg-white">
                <form onSubmit={handleSubmit} noValidate>
                  <div className="space-y-4">
                    <div>
                      <label
                        htmlFor="name"
                        className="block text-sm font-bold text-gray-700"
                      >
                        Name
                      </label>
                      <div className="mt-1">
                        <input
                          type="text"
                          name="name"
                          id="name"
                          value={name}
                          onChange={(event) => setName(event.target.value)}
                          className="block w-full px-4 py-3 rounded-md shadow-sm placeholder-gray-500 focus:ring-gray-500 focus:border-gray-500 border border-gray-300 focus:outline-none "
                          placeholder="John Doe"
                        />
                      </div>
                    </div>
                    <div>
                      <label
                        htmlFor="username"
                        className="block text-sm font-bold text-gray-700"
                      >
                        Username
                      </label>
                      <div className="mt-1">
                        <input
                          type="text"
                          name="username"
                          id="username"
                          value={username}
                          onChange={(event) => setUsername(event.target.value)}
                          className="block w-full px-4 py-3 rounded-md shadow-sm placeholder-gray-500 focus:ring-gray-500 focus:border-gray-500 border border-gray-300 focus:outline-none "
                          placeholder="johndoe"
                        />
                      </div>
                    </div>
                    <div>
                      <label
                        htmlFor="bio"
                        className="block text-sm font-bold text-gray-700"
                      >
                        Bio
                      </label>
                      <div className="mt-1">
                        <textarea
                          name="bio"
                          id="bio"
                          value={bio}
                          onChange={(event) => setBio(event.target.value)}
                          className="block w-full px-4 py-3 rounded-md shadow-sm placeholder-gray-500 focus:ring-gray-500 focus:border-gray-500 border border-gray-300 focus:outline-none "
                          rows="3"
                          placeholder="Tell us a little about yourself..."
                        ></textarea>
                      </div>
                    </div>
                    <div className="flex justify-end">
                      <button
                        type="submit"
                        className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
                        disabled={!record.isMutable || record.isMutating}
                      >
                        {record.isMutating ? "Updating..." : "Update Profile"}
                      </button>
                    </div>
                  </div>
                </form>
              </div>
            </div>
          </div>
        </main>
    </div>
  </>
  )
  
}

In the code above,

  • The component uses the useState hook to manage the state of three variables: name, bio, and username.

  • There's an async function called handleSubmit that is responsible for merging the current state of the variables into a record.

  • If any of the variables is empty, the handleSubmit function returns without updating the record witht he error message "Please fill out all fields".

You are almost there. Let's render the UI containing user record after its been updated.

//...


export default function Home() {
  //...

  return (
    <>
      <Head>
            {/* ... */}
      </Head>
      <div className="min-h-screen bg-gray-100">
        <nav className="bg-white shadow">
          {/* ... */}
        </nav>

        <main className="py-10">
        
              {/* ... */}
              
          {connection.status === "connected" && record && record.content ? (
            <div className="max-w-3xl mx-auto mt-8 sm:px-6 lg:px-8">
              <div className="bg-white overflow-hidden shadow sm:rounded-lg">
                <div className="px-4 py-5 sm:p-6">
                  <h2 className="text-lg font-medium text-gray-900 leading-6">
                    Your Profile
                  </h2>
                  <div className="mt-3 max-w-xl text-sm text-gray-500 grid grid-cols-2 gap-4">
                    <div className="flex flex-col">
                      <label className="text-gray-700 font-bold" htmlFor="name">
                        Name:
                      </label>
                      <p className="mb-1">{record.content.name}</p>
                    </div>
                    <div className="flex flex-col">
                      <label
                        className="text-gray-700 font-bold"
                        htmlFor="username"
                      >
                        Username:
                      </label>
                      <p className="mb-1">{record.content.username}</p>
                    </div>
                    <div className="col-span-2">
                      <label className="text-gray-700 font-bold" htmlFor="bio">
                        DID:
                      </label>
                      <p className="whitespace-pre-wrap mb-1">
                        {connection.selfID.id}
                      </p>
                    </div>
                    <div className="col-span-2">
                      <label className="text-gray-700 font-bold" htmlFor="bio">
                        Bio:
                      </label>
                      <p className="whitespace-pre-wrap mb-1">
                        {record.content.bio}
                      </p>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          ) : record &&
            !record.content &&
            !record.isLoading &&
            connection &&
            connection.status === "connected" ? (
            <div className="max-w-3xl mx-auto mt-8 sm:px-6 lg:px-8">
              <div className="bg-white overflow-hidden shadow sm:rounded-lg">
                <div className="px-4 py-5 sm:p-6">
                  <h2 className="text-lg font-medium text-gray-900 leading-6">
                    Profile Information
                  </h2>
                  <div className="mt-3 max-w-xl text-sm text-gray-500">
                    <p>
                      You don&apos;t have a profile yet. Create one by filling
                      out the form above.
                    </p>
                  </div>
                </div>
              </div>
            </div>
          ) : null}
          <Toaster
            position="top-center"
            reverseOrder={false}
            toastOptions={{ duration: 4000 }}
          />
        </main>
      </div>
    </>
  );
}

In the code snippet above:

  • This is a conditional statement that checks if the connection object has a status property that is set to "connected", and if the record object and its content property exist and are truthy. If these conditions are all true, it will render the HTML.
  • This HTML code renders a profile page for the user, displaying their name, username, DID (which may be a unique identifier), and bio.
  • Another condition checks if the record object exists and does not have a content property, and if it's not loading, and if the connection object exists and has a status property set to "connected". If all these conditions are true, it will render the HTML.

That's it. You can test out your application similar to what is shown below.

DID details

Kindly find the complete code on GitHub repository here.

Conclusion

In this workshop, you learn about building a Decentralized identity profile with Ethereum on Ceramic Networks.

Reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment