Empowering Web3 UX through Push Protocol's Notification System

A Step-by-Step Guide to Implementing Web3 Notifications

Empowering Web3 UX through Push Protocol's Notification System

Welcome to our step-by-step guide on integrating Push Protocol's notification system into your NextJS web application. In this blog, we'll cover the process of initializing a user, creating a notification channel, and implementing the APIs for sending and receiving notifications.

Initializing the User

Let's start by initializing the user with the following code snippet:

import { PushAPI, CONSTANTS } from "@pushprotocol/restapi";

const userAlice = await PushAPI.initialize(signer, {
  env: CONSTANTS.ENV.STAGING,
});

This code seamlessly initializes the user, creating a Push user profile object. If the profile doesn't exist, it is promptly generated, laying the foundation for subsequent function calls within the Push network.

Create Channel API

Next, let's create a channel using the provided code:

// userAlice.channel.create({options})
const response = await userAlice.channel.create({
  name: "Test Channel",
  description: "Test Description",
  icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAz0lEQVR4AcXBsU0EQQyG0e+saWJ7oACiKYDMEZVs6GgSpC2BIhzRwAS0sgk9HKn3gpFOAv3v3V4/3+4U4Z1q5KTy42Ql940qvFONnFSGmCFmiN2+fj7uCBlihpgh1ngwcvKfwjuVIWaIGWKNB+GdauSk8uNkJfeNKryzYogZYoZY40m5b/wlQ8wQM8TayMlKeKcaOVkJ71QjJyuGmCFmiDUe+HFy4VyEd57hx0mV+0ZliBlihlgL71w4FyMnVXhnZeSkiu93qheuDDFDzBD7BcCyMAOfy204AAAAAElFTkSuQmCC",
  url: "https://push.org",
});

This API is crucial for establishing a communication channel, allowing the system to send notifications. Pay attention to the parameters such as name, description, icon (base64 encoded), and URL, as they define the channel's characteristics.

NextJS Integration with TypeScript

To seamlessly integrate these functionalities into a NextJS frontend with TypeScript, we have a React component named Channel. This component serves as a unified form for creating a channel. You can follow along with the provided code, adjusting the types based on your application needs.

// @ts-nocheck
import React, { useState, useEffect } from 'react';
import { PushAPI, CONSTANTS } from "@pushprotocol/restapi";


interface ChannelProps {
  signer: any; // Adjust the type of signer based on your application
}

const Channel: React.FC<ChannelProps> = ({ signer }) => {
  // State to hold form data
  const [formData, setFormData] = useState({
    name: '',
    description: '',
    url: '',
    icon: ''
    // delegateAddresses: null,
  });

  // State to    added delegates
  // const [addedDelegates, setAddedDelegates] = useState([]);

  // Function to handle form input changes
  const handleInputChange = (e) => {
    setFormData({
      ...formData,
      [e.target.name]: e.target.value,
    });
  };



  // Function to handle form submission
  const handleSubmit = async () => {
    const push_signer = await PushAPI.initialize(signer, { env: CONSTANTS.ENV.STAGING });
    try {
      if (signer) {
        const response = await push_signer.channel.create({
          name: formData.name,
          description: formData.description,
          url: formData.url,
          icon: formData.icon
        });
        console.log("Channel Created", response);
        // Rest of your code...
      } else {
        console.error('Signer or signer.channel.create is undefined');
      }
    } catch (error) {
      // Handle errors here
      console.error(error);
    }
  };

  // useEffect to log the form data whenever it changes (for debugging purposes)
  useEffect(() => {
    console.log('Form Data:', formData);
  }, [formData]);

  useEffect(() => { handleSubmit }, [signer]);

  // useEffect to log the added delegates whenever it changes (for debugging purposes)
  // useEffect(() => {
  //   console.log('Added Delegates:', addedDelegates);
  // }, [addedDelegates]);

  return (
    <div className="flex flex-col items-center">
      <label className="mb-2" htmlFor="name">
        Channel Name:
      </label>
      <input
        type="text"
        id="name"
        name="name"
        value={formData.name}
        onChange={handleInputChange}
        className="border p-2 mb-4"
      />

      <label className="mb-2" htmlFor="description">
        Channel Description:
      </label>
      <textarea
        id="description"
        name="description"
        value={formData.description}
        onChange={handleInputChange}
        className="border p-2 mb-4"
      />

      <label className="mb-2" htmlFor="url">
        URL
      </label>
      <textarea
        id="url"
        name="url"
        value={formData.url}
        onChange={handleInputChange}
        className="border p-2 mb-4"
      />

      <label className="mb-2" htmlFor="icon">
        ICON
      </label>
      <textarea
        id="icon"
        name="icon"
        value={formData.icon}
        onChange={handleInputChange}
        className="border p-2 mb-4"
      />


      {/* <label className="mb-2" htmlFor="delegateAddresses">
                Delegate Addresses (comma-separated):
              </label>
              <input
                type="text"
                id="delegateAddresses"
                name="delegateAddresses"
                value={formData.delegateAddresses}
                onChange={handleInputChange}
                className="border p-2 mb-4"
              /> */}

      <button
        onClick={handleSubmit}
        className="bg-blue-500 text-white p-2 rounded hover:bg-blue-700"
      >
        Save
      </button>
    </div>
  );
};

export default Channel;

Sending Notifications

Now, let's explore the APIs for sending notifications:

// @ts-nocheck
import React, { useState } from 'react';

const NotificationSender = ({ onNotificationSent }) => {
    const [notificationBody, setNotificationBody] = useState('');

    const handleInputChange = (e) => {
        setNotificationBody(e.target.value);
    };

    const handleSendNotification = async () => {
        try {
            const sendNotifRes = await userAlice.channel.send(["*"], {
                notification: { title: "test", body: notificationBody },
            });

            onNotificationSent(sendNotifRes);

            setNotificationBody('');
        } catch (error) {
            console.error(error);
        }
    };

    return (
        // Render the notification sending form
    );
};

export default NotificationSender;

This component allows users to input a notification body and send it through the Push protocol. The onNotificationSent callback is invoked to notify the parent component.

Viewing Notifications

Finally, let's implement the API for fetching notifications:

import React, { useState } from 'react';

const NotificationDisplay = ({ notification }) => {
    const [shownNotification, setShownNotification] = useState(notification);

    // useEffect to update shownNotification whenever a new notification is received

    return (
        // Render the notification display component
    );
};

export default NotificationDisplay;

Surprise code

The provided code exemplifies a sophisticated notification system integrated seamlessly into a React application. Leveraging the capabilities of Push Protocol's notification system, this solution empowers users to receive real-time notifications in a Web3 environment.

The implementation involves initializing a user through the Ethereum provider, creating a channel, and establishing a connection to the notification stream. As the stream connects, a simulated notification is sent, showcasing the system's capability to capture, process, and display notifications. The progress information dynamically updates, providing users with valuable insights into the notification process.

Moreover, the code incorporates the UIWeb/NotificationItem component to elegantly render notifications. The NotificationInterface function encapsulates the user interface, offering a user-friendly experience by displaying wallet-specific notification items.

This solution serves as a powerful tool for developers seeking to enhance user engagement by integrating a robust and efficient notification system into their Web3 applications. With its comprehensive features, including real-time updates, streamlined UI rendering, and clear progress tracking, this code is poised to elevate the overall user experience in the Web3 ecosystem.


import { useState, useEffect, useRef } from "react";
import { ethers } from "ethers";
import { PushAPI, CONSTANTS } from "@pushprotocol/restapi";
import { NotificationItem, chainNameType } from "@pushprotocol/uiweb";

interface NotificationItemData {
  cta: string;
  title: string;
  message: string;
  app: string;
  icon: string;
  image: string;
  url: string;
  blockchain: string;
  notification: string;
}

interface NotifItem extends NotificationItemData {
  key: number;
}

export function Notification2(props: any) {
  const [wallet, setWallet] = useState<string>(
    "0xD8634C39BBFd4033c0d3289C4515275102423681"
  );
  const [progressTexts, setProgressTexts] = useState<string[]>([]);
  const [notifItems, setNotifItems] = useState<NotifItem[]>([]);

  const walletRef = useRef<HTMLInputElement | null>(null);

  useEffect(() => {
    if (walletRef.current) {
      walletRef.current.value = wallet;
    }
  }, [wallet]);

  const triggerNotification = async () => {
    // Demo only supports MetaMask (or other browser based wallets) and gets provider that injects as window.ethereum into each page
    const provider = new ethers.providers.Web3Provider(window.ethereum);

    // Switch to sepolia
    await provider.send("wallet_switchEthereumChain", [{ chainId: "0xAA36A7" }]);

    // Get provider
    await provider.send("eth_requestAccounts", []);

    // Grabbing signer from provider
    const signer = provider.getSigner();

    // Initialize user for push
    const userAlice = await PushAPI.initialize(signer, { env: CONSTANTS.ENV.STAGING });

    // establish connection to stream
    const stream = await userAlice.initStream([CONSTANTS.STREAM.CONNECT, CONSTANTS.STREAM.NOTIF]);

    // Listen for stream connection
    stream.on(CONSTANTS.STREAM.CONNECT, async (data) => {
      console.log("STREAM CONNECTED");
      let text = ['Stream Connected...', 'Sending Simulated Notification...', 'Wait for few moments for stream to capture notif and display...', 'Waiting for you to sign notification payload...'];
      setProgressTexts(text);
      await userAlice.channel.send([userAlice.account], {
        notification: { 
          title: 'GM Builders!', 
          body: `_Simulated notification_ listened by stream and rendered with **@UIWeb/NotificationItem** with the latest timestamp - ${new Date().valueOf()} [timestamp: ${new Date().valueOf()}]` 
        },
        payload: {
          title: 'GM Builders!', 
          body: `_Simulated notification_ listened by stream and rendered with **@UIWeb/NotificationItem** with the latest timestamp - ${new Date().valueOf()} [timestamp: ${new Date().valueOf()}]`,
          cta: 'https://push.org',
          embed: 'https://push.org/assets/images/cover-image-8485332aa8d3f031e142a1180c71b341.webp',
        }
      });
      text.push('Message generated and sent. Waiting for the stream to pick it up...');
      setProgressTexts(text);
    });

    // Listen for notifications
    stream.on(CONSTANTS.STREAM.NOTIF, (item) => {
      let text = progressTexts;
      console.log(item);
      text.push('Notification Received...');
      text.push(JSON.stringify(item));
      setProgressTexts(text);

      // create notification item compatible with UIWeb/NotificationItem
      const compatibleNotifItem = {
        title: item.message.payload.title,
        message: item.message.payload.body,
        image: item.message.payload.embed,
        cta: item.message.payload.cta,
        icon: item.channel.icon,
        app: item.channel.name,
        url: item.channel.url,
        blockchain: item.source,
        notification: item.message.notification,
      };
      setNotifItems([compatibleNotifItem]);
    });

    // connect stream
    stream.connect();
  };

  function NotificationInterface() {
    const inputStyle = {
      padding: "10px",
      margin: "10px 0",
      width: "100%",
      boxSizing: "border-box",
    };

    const textareaStyle = {
      ...inputStyle,
      height: "100px",
      resize: "vertical",
    };

    const buttonStyle = {
      padding: "10px 20px",
      backgroundColor: "#dd44b9",
      color: "#FFF",
      border: "none",
      borderRadius: "5px",
      cursor: "pointer",
      marginTop: "20px",
    };

    return (
      <div style={{ width: "auto", margin: "20px auto" }}>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <div style={{ flex: 1 }}>
            <h2>
              Push based mechanism for displaying notifications on frontend
            </h2>
            <p />
            <label>
              Put any wallet address and click on fetch notifications to see the
              live results. Click to expand <b>Live Editor</b> tab to see the
              code and play with it. For this demo, You will need Metamask (or
              equivalent browser injected wallet), you will also need to sign a
              transaction to see the notifications.
            </label>
            <p />
          </div>
        </div>
        <div>
          <hr />
          <h3>Progress (will show progress information once Trigger Notification is clicked)</h3>

          {progressTexts.map((text, idx) => {
            return (
              <React.Fragment key={idx}>
                <span>{text}</span>
                <br />
              </React.Fragment>
            );
          })}
        </div>
        <hr />
        <button style={buttonStyle} onClick={triggerNotification}>
          Trigger Notification
        </button>

        <p />
        <p />

        {notifItems.length > 0 ? (
          <h3>{`Notification Items for ${wallet}`}</h3>
        ) : (
          <></>
        )}

        {notifItems.map((notifItemSingular, idx) => {
          const {
            cta,
            title,
            message,
            app,
            icon,
            image,
            url,
            blockchain,
            notification,
          } = notifItemSingular;

          return (
            <NotificationItem
              key={idx} // any unique id
              notificationTitle={title}
              notificationBody={message}
              cta={cta}
              app={app}
              icon={icon}
              image={image}
              url={url}
              theme={"light"} // or can be dark
              chainName={blockchain as chainNameType} // if using Typescript
            />
          );
        })}
      </div>
    );
  }

  return (
    <>
      <NotificationInterface />
    </>
  );
}

export default Notification2;

Conclusion

In conclusion, the incorporation of the Notification2 component results in a refined and structured presentation of received notifications, significantly augmenting the overall user experience. This component plays a pivotal role in streamlining the notification handling process within your NextJS web application.

To sum up, by adhering to this guide and seamlessly integrating the provided code snippets, you can empower your NextJS web application with a resilient notification system. This not only enriches your application's functionality but also ensures a seamless and engaging user experience within the dynamic landscape of Web3 environments.


Thank you for exploring the implementation of a powerful notification system in your NextJS web application through this blog. If you found the insights valuable, please consider sharing it with your network. For more tech-related content and updates, follow me on Hashnode. Your support is appreciated!

Cheers

Shaikh Rumman Fardeen

Push Protocol Ambassador

Follow me:

Linkedin : https://www.linkedin.com/in/srummanf

Github : https://github.com/srummanf

Twitter : https://twitter.com/srummanf