# Create a Paywall on a Next.js Blog Using Fingerprint and Sanity

> Learn how to add a paywall to a blog built with Next.js using Fingerprint, Firebase, and Sanity.

**Source:** https://hackmamba.io/engineering/create-a-paywall-on-a-next-js-blog-using-fingerprint/
**Published:** 16 Jan 2023
**Author:** Asjad Khan
**Category:** Engineering

---
A paywall is a system that restricts user access to particular web content until the user subscribes to it. Paywalls are often used on journal sites, blogs, and daily newspaper sites.

In this post, we'll learn to implement a paywall on a blog using Fingerprint.js, Sanity, and Next.js. The paywall will block users from accessing the blog posts after the user has opened more than three posts.

[Fingerprint](https://fingerprint.com/?utm_source=hackmamba&utm_medium=hackmamba-blog) is a browser fingerprinting software that accurately retrieves a user's browser information. The information is then used to assign a unique identifier to the user, even in a private/incognito browsing mode. 

We will fetch the blog page contents from [Sanity](https://sanity.io?utm_source=hackmamba&utm_medium=hackmamba-blog). This post will not cover how to set up content in Sanity, but interested readers can quickly watch [this video resource](https://www.youtube.com/watch?v=32RP-sG1njE&t=6s) about getting started with Sanity. Also, we will use [Firebase](https://firebase.google.com/?utm_source=hackmamba&utm_medium=hackmamba-blog) to store visitor activities while on the page; learn how to quickly get started with firebase using [this resource](https://dev.to/kizmelvin/store-and-retrieve-data-in-firebase-cloud-storage-with-react-js-fff).

This is a live project [Demo](https://fingerprint-blog-blocker.netlify.app/), and the complete source code can be found in the following GitHub repository. 


https://github.com/Kizmelvin/fingerprintJS-paywall


## Prerequisites

This post requires the following:

- Experience with JavaScript and React.js.
- An installation of Node.js.
- A Fingerprint account. [Signup](https://fingerprint.com/?utm_source=hackmamba&utm_medium=hackmamba-blog) is free!


## Getting Started with Next.js

[Next.js](https://nextjs.org/) is a React-based frontend development framework that supports server-side rendering and static site generation.

Run the following command in the terminal to create a new Next.js application:

```shell
npx create-next-app blog-paywall
```

The above command creates a starter Next.js application in the **blog-paywall** folder.

Next, we'll navigate into the project directory and start the application with the following commands:

```shell
cd blog-paywall # to navigate into the project directory
npm run dev # to run the dev server
```

Next.js will start a live development server at [http://localhost:3000](http://localhost:3000).

## Setting up Fingerprint.js

[Login](https://dashboard.fingerprint.com/login?utm_source=hackmamba&utm_medium=hackmamba-blog) to Fingerprint and click the “+ NEW APPLICATION” button, choose the app name, provide a domain, and then click the CREATE button.

![](https://paper-attachments.dropbox.com/s_A30FF9483281F76009406C615C6B767F8612DF9A113E77C41CA4FA984A2C9EB7_1663810536776_Screenshot+2022-09-22+at+02.17.10.png)


On the next screen, review the app details and proceed to create the app.

From the home page, click on the `settings icon` and select “`Account settings`.” On the next screen, scroll down and navigate to `React`; copy the snippets and save them. Th it will come in handy when building our application.


![](https://paper-attachments.dropbox.com/s_A30FF9483281F76009406C615C6B767F8612DF9A113E77C41CA4FA984A2C9EB7_1663811741066_Screenshot+2022-09-22+at+02.40.53.png)
![](https://paper-attachments.dropbox.com/s_A30FF9483281F76009406C615C6B767F8612DF9A113E77C41CA4FA984A2C9EB7_1663811658005_Screenshot+2022-09-22+at+02.43.17.png)



## Building the blog paywall

We’ll start by installing the following npm packages to add Fingerprint, Firebase, and manage Sanity content:

- [Fingerprint.js](https://www.npmjs.com/package/@fingerprintjs/fingerprintjs-pro-react) and [Firebase](https://www.npmjs.com/package/firebase).
- [@sanity/image-url](https://www.npmjs.com/package/@sanity/image-url) and [@sanity/block-content-to-react](https://www.npmjs.com/package/@sanity/block-content-to-react)
- [bootstrap](https://www.npmjs.com/package/bootstrap) and [react-bootstrap](https://www.npmjs.com/package/react-bootstrap).

Install the above dependencies with the command below:

```shell
npm install @fingerprintjs/fingerprintjs-pro-react firebase @sanity/image-url @sanity/block-content-to-react bootstrap react-bootstrap 
```

Next, in the root directory of our project, go to the **pages** folder and modify the `_app.js` file with the snippets we got from fingerprint, as shown below:

```js
// pages/_app.js 
import "../styles/globals.css";
import { FpjsProvider } from "@fingerprintjs/fingerprintjs-pro-react";
function MyApp({ Component, pageProps }) {
  return (
    <FpjsProvider
      loadOptions={{
        apiKey: "<Your API Key>",
      }}
    >
      <Component {...pageProps} />
    </FpjsProvider>
  );
}
export default MyApp;
```

Here, we imported `FpjsProvider`, used to wrap our topmost component, and passed our **API Key** to it.

## Fetching and rendering Blog Posts

Let's clean up the `index.js` file and update it with the following snippets:

```js
// pages/index.js
import styles from "../styles/Home.module.css";

export const getStaticProps = async (pageContext) => {
  const allPosts = encodeURIComponent(`*[ _type == "post"]`);
  const url = `https://dlwalt36.api.sanity.io/v1/data/query/production?query=${allPosts}`;
  const getPosts = await fetch(url).then((res) => res.json());
  if (!getPosts.result || !getPosts.result.length) {
    return {
      posts: [],
    };
  } else {
    return {
      props: {
        posts: getPosts.result,
      },
    };
  }
};

export default function Home({ posts }) {
  
  return (
    <div className={styles.main}>
      <h1>Welcome to Blog Page</h1>
    </div>
  );
}
```    

In the snippets above, we fetched our blog post contents from Sanity with the `getStaticProps()` function and returned the response so that the `Home` component could access it.

Next, let’s read the post contents with the following snippets:

```js
// pages/index.js
import styles from "../styles/Home.module.css";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import imageUrlBuilder from "@sanity/image-url";

export default function Home({ posts }) {
  const [receivedPosts, setReceivedPosts] = useState([]);
  const router = useRouter();

  useEffect(() => {
    if (posts.length) {
      const imgBuilder = imageUrlBuilder({
        projectId: "<YOUR PROJECY_ID>",
        dataset: "production",
      });
      setReceivedPosts(
        posts.map((post) => {
          return {
            ...post,
            mainImage: imgBuilder.image(post.mainImage),
          };
        })
      );
    } else {
      setReceivedPosts([]);
    }
  }, [posts]);

// return() function here
}
```    

In the snippets above, we:

- Imported `useState`, `useEffect`, `useRouter`, and `imageUrlBuilder`; used the `useState` and created `receivedPosts` state constant.
- Created a constant `router`, called the `useRouter()` function on it, looped through the posts data, and updated the `receivedPosts` state.

Let’s display the blog post on the frontend. Modify the `index.js` file with the following snippets:

```js
//pages/index.js
//imports here
export default function Home({ posts }) {
const [receivedPosts, setReceivedPosts] = useState([]);
const router = useRouter();

  // useEffect() function here

  return (
    <div className={styles.main}>
      <h1>Welcome to Blog Page</h1>
      <div className={styles.feed}>
        {receivedPosts.length ? (
          receivedPosts.map((post, index) => (
            <div
              key={index}
              className={styles.post}
              onClick={() => router.push(`/post/${post.slug.current}`)}
            >
              <img
                className={styles.img}
                src={post.mainImage}
                alt="post thumbnail"
              />
              <h3>{post.title}</h3>
            </div>
          ))
        ) : (
          <>No Posts</>
        )}
      </div>
    </div>
  );
}
```

Here, we used conditional rendering, looped through the `receivedPosts` state, and displayed the blog posts in `return()` function.

In the browser, we’ll have the application, as shown below:

![](https://paper-attachments.dropbox.com/s_A30FF9483281F76009406C615C6B767F8612DF9A113E77C41CA4FA984A2C9EB7_1663853423363_Screenshot+2022-09-22+at+14.30.11.png)

## Adding Dynamic Route

Now, we want to open each post and view its content. In the **pages** directory, let’s create a **post** folder and create a `[slug].js` file for dynamic routing by implementing the following snippets:

```js
// pages/post/[slug].js
const BlockContent = require("@sanity/block-content-to-react");
import SyntaxHighlighter from "react-syntax-highlighter";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import imageUrlBuilder from "@sanity/image-url";
import styles from "../../styles/Home.module.css";
import { useVisitorData } from "@fingerprintjs/fingerprintjs-pro-react";
import {
  updateDoc,
  doc,
  arrayUnion,
  setDoc,
  getDocFromServer,
} from "firebase/firestore";
import { db } from "../../Utils";

export const getServerSideProps = async (pageContext) => {
  const pageSlug = pageContext.query.slug;
  const particularPost = encodeURIComponent(
    `*[ _type == "post" && slug.current == "${pageSlug}" ]` );
  const url = `https://<YOUR PROJECT_ID>}.api.sanity.io/v1/data/query/production?query=${particularPost}`;
  const postData = await fetch(url).then((res) => res.json());
  const postItem = postData.result[0];
  if (postItem) {
    return {
      props: {
        title: postItem.title,
        image: postItem.mainImage,
        body: postItem.body,
        slug: postItem.slug.current,
      },
    };
  } else {
      return {
      notFound: true,
    };
  }
};

export default function Post({ title, body, image, slug }) {
  const [modal, setModal] = useState(false);
  const [updated, setUpdated] = useState(false);
  const [visitTimes, setVisitTimes] = useState(0);
  const [imageUrl, setImageUrl] = useState();
  const { getData } = useVisitorData({ immediate: true });
  
  const router = useRouter();
  return (
    <>
    <h1>Hello from single post </h1>
    </>
  );
}
```

In the snippets above, we did the following:

- Added `BlockContent` and `SyntaxHighlighter` — to help us render the post contents and the code snippets.
- Added `useState` and `useEffect` hooks from "react" and `useRouter` from "next/router."
- Imported `imageUrlBuilder` to handle the blog post images and `useVisitorData` to track users' information.
- Imported different methods from "firebase/firestore" and `db` from our firebase configuration in the `Utils.js` file.

In the `getServerSideProps()` function, we fetched our blog content again from Sanity and returned the response.

We destructured the `title`, `body`, `image`, and `slug` and created `modal`, `updated`, `visitTimes`, and `imageUrl` constants with the `useState` hook in the post component function.

Furthermore, we destructured `getData` from `useVisitorData`, created a constant `router`, and called the `useRouter()` function on it. 

Next, we'll use the properties we destructured from the post component function and implement the single post page. Update the `[slug].js` file with the following snippets:

```js
//pages/post/slug.js

const BlockContent = require("@sanity/block-content-to-react");
// imports here
// getServerSideProps() function here

function Post({ title, body, image, slug }) {
  // constants here

  useEffect(() => {
    const imgBuilder = imageUrlBuilder({
      projectId: "<your project_id>",
      dataset: "production",
    });
    setImageUrl(imgBuilder.image(image));
  }, [image]);
  
const serializers = {
    types: {
      code: (props) => (
        <div className="my-2">
          <SyntaxHighlighter language={props.node.language}>
            {props.node.code}
          </SyntaxHighlighter>
        </div>
      ),
    },
  };
  // return() function here
}

export default Post;
```

In the snippets above:

- We used the `imageUrlBuilder` to quickly generate image URLs from Sanity image records and set our `imageUrl` to the generated image in the `useEffect()` function.
- Created a `serializer` object inside it and used `SyntaxHighlighter` to highlight code snippets in the post content.

Now, in the return function, let’s display the post contents and update the `[slug].js` file with the following snippets: 

```js
// pages/post/[slug].js
const BlockContent = require("@sanity/block-content-to-react");
// imports here

// getServerSideProps() function here

function Post({ title, body, image, slug }) {
  // constants here

// useEffect() function here

  // serializer object here
  return (
    <>
      <div className={styles.postItem}>
        <div className={styles.postNav} onClick={() => router.push("/")}>
          &#x2190;
        </div>
        {imageUrl && <img src={imageUrl} alt={title} />}
        <div>
          <h1>
              <strong>{title}</strong>
          </h1>
        </div>
        <div className={styles.postBody}>
          <BlockContent
            blocks={body}
            serializers={serializers}
            imageOptions={{ w: 320, h: 240, fit: "max" }}
            projectId={"Your project_ID"}
            dataset={"production"}
          />
        </div>
      </div>
    </>
  );
}
export default Post;
```

Here, we displayed the blog post `title` and `Image`, used `BlockContent` to show the post body, and passed `serializers` to it.

At this point, a user can click on a particular post and view its contents in the browser.

![](https://paper-attachments.dropbox.com/s_A30FF9483281F76009406C615C6B767F8612DF9A113E77C41CA4FA984A2C9EB7_1663925212368_ezgif.com-gif-maker+3.gif)


## Storing user activity in Firebase

We also want to track the user information using Fingerprint and save the user ID and posts visited on Firebase. Learn how to store and retrieve data and configure Firebase using [this resource](https://dev.to/kizmelvin/store-and-retrieve-data-in-firebase-cloud-storage-with-react-js-fff).

After a user has viewed up to three posts, a paywall will block the user from further visits until they subscribe. To do so, update the `[slug].js` file with the following snippets:

```js
// pages/post/[slug].js
const BlockContent = require("@sanity/block-content-to-react");
// imports here
// getServerSideProps() function here

function Post({ title, body, image, slug }) {
  // constants here

    useEffect(() => {
    visitedTimes();
    const imgBuilder = imageUrlBuilder({
      projectId: "<your project_id>",
      dataset: "production",
    });
    setImageUrl(imgBuilder.image(image));
  }, [image, updated, modal]);

  // serializer object here

  const visitedTimes = async () => {
    await getData().then(async (visitor) => {
      const visited = {
        visitorId: visitor.visitorId,
        visitedPostId: slug,
      };
      const { visitorId, visitedPostId } = visited;
      const visitorRef = doc(db, "visitors", `${visitorId}`);
      const documentSnap = await getDocFromServer(visitorRef);
      if (documentSnap.exists()) {
        await updateDoc(visitorRef, {
          visitedPosts: arrayUnion(visitedPostId),
        });
        setUpdated(true);
        if (documentSnap.data().visitedPosts.length >= 3) {
          setModal(true);
        }
        setVisitTimes(documentSnap.data().visitedPosts.length);
      } else {
        setDoc(visitorRef, {
          visitedPosts: visitedPostId,
        });
      }
    });
  };
  //return() function here
}
export default Post;
```

Here, we created the `visitedTimes()` function that will get the user ID and posts visited and save them in Firebase. When the user reads up to three posts, the function updates the modal state to true.


## Adding a paywall

In the `return()` function, let’s conditionally render the paywall to block the user when they have visited more than three posts.

```js
// pages/post/[slug].js
//Other imports here
import Modal from "react-bootstrap/Modal";
import Link from "next/link";
//getSeverSideProps() function here

function Post({ title, body, image, slug }) {
// constants here
  // visitedTimes() function here
  // useEffect() function here
  //serializers object here
  return (
    <>
      {visitTimes >= 3 && modal ? (
        <Modal
          centered
          show={modal}
          onHide={() => window.location.href("/")}
          animation={true}
        >
          <Modal.Header>
            <Modal.Title>Modal heading</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            Oops! Seems you have exceeded your allocated free articles. You can
            get back by subscribing
          </Modal.Body>
          <Modal.Footer>
            <Link role="button" className="btn btn-secondary" href="/">
              Go Home
            </Link>
            <Link className="btn btn-secondary" href="#">
              Pay Now
            </Link>
          </Modal.Footer>
        </Modal>
      ) : (
        <div className={styles.postItem}>
        //single post here
        </div>
      )}
    </>
  );
}
export default Post;
```

In the snippets above, we imported `Link` from "next/link" and `Modal` from "bootstrap/Modal", and then we conditionally rendered the Modal in the components' `return()` function.

Now, the paywall will block the user from reading the blog posts after the third one.


![](https://paper-attachments.dropbox.com/s_A30FF9483281F76009406C615C6B767F8612DF9A113E77C41CA4FA984A2C9EB7_1664273794383_ezgif.com-gif-maker+4.gif)


## Conclusion 

This post discussed adding a paywall in a Next.js blog with Fingerprint and Firebase.

## Resources 

The following resources might be helpful:

- [Introduction to fingerprint.js](https://dev.fingerprint.com/docs/introduction?utm_source=hackmamba&utm_medium=hackmamba-blog). 
- [Store and retrieve data in firebase](https://dev.to/kizmelvin/store-and-retrieve-data-in-firebase-cloud-storage-with-react-js-fff)
- [Getting started with Sanity](https://dev.to/zacherymorgan/getting-started-with-sanity-1ohj)
