Getting Started With Lens Protocol As A Frontend Developer

Getting Started With Lens Protocol As A Frontend Developer

How To Setup A Login And A Feed Of Recommended Posts With Lens Protocol And React.js

It is safe to say that Lens Protocol has been the talk of the town for these past few months in web3. It is the closest thing we have to a decentralized and composable social graph.

The Lens team has conveniently made available an API that indexes all of the contracts on the protocol and gives developers, especially frontend devs ease of access to the protocol's data. This API relies on JWT tokens to authenticate users. Our focus for this article will be on learning about how these tokens are used by Lens Protocol to authenticate users and how you can set up a simple user authentication flow on your frontend.

Building An Authenticated Lens Protocol Feed

We’ll be building a Next.js app that lets people connect their wallets and then logs into Lens Protocol. As a bonus, we will also show them a timeline of some recommended posts coming straight from the Lens API!

https://i.imgur.com/X1rt7ta.jpg

Project Setup Walkthrough

Let's start by setting up a Next.js project locally. To save us some time, we are going to use a Next RainbowKit wagmi template I made as a base. It comes along with Rainbowkit, wagmi and Chakra UI pre-configured.

git clone https://github.com/Dhaiwat10/next-chakra-rainbowkit-wagmi-starter lens-frontend;

Let's install the dependencies now.

cd lens-frontend;

yarn;

We also need to install two additional dependencies. We will be needing these to fetch data from the Lens API.

# ./lens-frontend

yarn add @apollo/client graphql

Now, let’s look at what we’re working with by running yarn dev and opening our site on http://localhost:3000.

https://i.imgur.com/LwW0BFm.png

API Client Setup

Let's get to business! As discussed earlier, we will be using Apollo Client to interact with the Lens API.

Create a new file called utils.js at the root of your project. We'll start by setting up an HttpLink to fetch data and an ApolloLink to take care of authentication. In the end, we are just creating an ApolloClient and passing in both the httpLink and authLink.

File: ./utils.js

// utils.js
import {
  ApolloClient,
  ApolloLink,
  HttpLink,
  InMemoryCache,
  gql
} from '@apollo/client';

const API_URL = '<https://api.lens.dev/>';

// `httpLink` our gateway to the Lens GraphQL API. It lets us request for data from the API and passes it forward
const httpLink = new HttpLink({ uri: API_URL });

/* `authLink` takes care of passing on the access token along with all of our requests. We will be using session storage to store our access token. 

The reason why we have to account for an access token is that that's what the Lens API uses to authenticate users. This is the token you'll get back when someone successfully signs in. We need to pass this token along with all the requests we made to the API that *need* authentication.
*/
const authLink = new ApolloLink((operation, forward) => {
    const token = sessionStorage.getItem('accessToken');

    operation.setContext({
        headers: {
            'x-access-token': token ? `Bearer ${token}` : '',
        },
    });

    return forward(operation);
});

export const apolloClient = new ApolloClient({
    link: authLink.concat(httpLink),
    cache: new InMemoryCache(),
});

Sign-In With Lens Setup

The next step is to get a brief idea of the general flow of how authentication with the Lens API works.

https://i.imgur.com/mg6snvy.png

1. Request For A “Challenge” Text

To verify the authenticity of requests coming in, the Lens API asks clients to request for a “challenge” text (or prompt) that they must get signed by the user's wallet. This challenge text is nothing but a piece of text containing a timestamp and a random request id (nonce) generated by the API. This is what a challenge text looks like:

https://i.imgur.com/Wq03fIq.png

Before prompting the user to sign this message with their wallet, we have to retrieve it from the Lens API. Let's set up a query for that. Add this to the bottom of your utils.js file.

File: ./utils.js

// ...

const GET_CHALLENGE = `
    query($request: ChallengeRequest!) {
        challenge(request: $request) { text }
    }
`;

export const generateChallenge = async (address) => {
    const res = await apolloClient.query({
        query: gql(GET_CHALLENGE),
        variables: {
            request: {
                address,
            }
        }
    });
    return res.data.challenge.text;
}

The generateChallenge function lets us ask the Lens API for a challenge text for the user to sign. Let's now add an actual sign in button to our UI. Go to the pages/index.js file and add this to the markup:

File: ./pages/index.js

export default function Home() {
  return (
    <Container paddingY='10'>
      <ConnectButton />

      <Button marginTop='2'>Login with Lens</Button>
    </Container>
  );
}

Next, we are going to create a signIn function and execute it whenever someone clicks the Login button.

import { Button, Container } from '@chakra-ui/react';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useAccount } from 'wagmi';
import { generateChallenge } from '../utils';

export default function Home() {
  const { data } = useAccount();
  const address = data?.address;
  const connected = !!data?.address;

  const signIn = async () => {
    try {
      if (!connected) {
        return alert('Please connect your wallet first');
      }
      const challenge = await generateChallenge(address);
      console.log({ challenge });
    } catch (error) {
      console.error(error);
      alert('Error signing in');
    }
  };

  return (
    <Container paddingY='10'>
      <ConnectButton />

     <Button onClick={signIn} marginTop='2'>
       Login with Lens
     </Button>
     <Button marginTop='2'>Login with Lens</Button>
    </Container>
  );
}

You should now be able to click on the Login button and see a challenge text being logged in the console.

https://i.imgur.com/pgTLNmu.gif

2. Ask The User To Sign The Challenge Text

We will use the useSignMessage hook from wagmi to do this. All we need to do is to pass in the challenge to the signMessageAsync function and retrieve the signature.

File: ./pages/index.js

import { useAccount, useSignMessage } from 'wagmi';

export default function Home() {
  const { data } = useAccount();
  const address = data?.address;
  const connected = !!data?.address;

    // `signMessageAsync` lets us programatically request a message signature from the user's wallet
 const { signMessageAsync } = useSignMessage();

  const signIn = async () => {
    try {
      if (!connected) {
        return alert('Please connect your wallet first');
      }
      const challenge = await generateChallenge(address);
      const signature = await signMessageAsync({ message: challenge });
      console.log({ signature });
    } catch (error) {
      console.error(error);
      alert('Error signing in');
    }
  };

  return (
    <Container paddingY='10'>
      <ConnectButton />

     <Button onClick={signIn} marginTop='2'>
       Login with Lens
     </Button>
     <Button marginTop='2'>Login with Lens</Button>
    </Container>
  );
}

You should now see a Metamask popup asking you to sign a message and the signed message being logged in the console.

https://i.imgur.com/FiGaS5p.gif

3. Retrieve Access Token With Signed Message

Now that we have a signature, let's send it over to the Lens API for verification. In return, we can expect to get back an access token if the signature is legit.

Go to your utils.js file and add this piece of code to the bottom of your file.

File: ./utils.js

// ...

const AUTHENTICATION = `
  mutation($request: SignedAuthChallenge!) {
    authenticate(request: $request) {
      accessToken
      refreshToken
    }
 }
`;

export const authenticate = async (address, signature) => {
  const { data } = await apolloClient.mutate({
    mutation: gql(AUTHENTICATION),
    variables: {
      request: {
        address,
        signature,
      },
    },
  });
  return data.authenticate.accessToken;
};

The authenticate function here is taking in the user's address and the signed message, sending it to the Lens API to get it verified and returning an access token if there is one. We will now make use of this function inside of the signIn function for our UI in our pages/index.js file.

File: ./pages/index.js

import { authenticate, generateChallenge } from '../utils';

export default function Home() {
  const { data } = useAccount();
  const address = data?.address;
  const connected = !!data?.address;
  const { signMessageAsync } = useSignMessage();

  const signIn = async () => {
    try {
      if (!connected) {
        return alert('Please connect your wallet first');
      }
      const challenge = await generateChallenge(address);
      const signature = await signMessageAsync({ message: challenge });
      const accessToken = await authenticate(address, signature);
      console.log({ accessToken });
      window.sessionStorage.setItem('accessToken', accessToken);
    } catch (error) {
      console.error(error);
      alert('Error signing in');
    }
  };

  return (
    <Container paddingY='10'>
      <ConnectButton />

     <Button onClick={signIn} marginTop='2'>
       Login with Lens
     </Button>
     <Button marginTop='2'>Login with Lens</Button>
    </Container>
  );
}

You will also notice that we are calling window.sessionStorage.setItem to store the access token to the browser's session storage. Let me explain why - if you go back to the utils.js file and have a look, you will notice that we are looking for the access token inside the browser's session storage to then pass it along with any further requests.

File: ./utils.js


// ...

const authLink = new ApolloLink((operation, forward) => {
  const token = sessionStorage.getItem('accessToken');

  operation.setContext({
    headers: {
      'x-access-token': token ? `Bearer ${token}` : '',
    },
  });

  return forward(operation);
});

Let's now go to our browser and try this out. You should now see the access token being stored in the browser's session storage successfully.

https://i.imgur.com/z3CdmmH.gif

That's it! That's all you need to do to integrate Login with Lens in your React app. With the setup that we have in place, you don't need to do any additional work in order to make use of the access token in your future API requests. (You may need to tackle refresh tokens if you want to create a production app)

ℹ️ Note: Lens API currently has restricted access to the write endpoints/mutations. You might need to get your domain allowlisted.

Bonus: Timeline

As a bonus, we will add a simple timeline showing the current top posts on Lens Protocol.

To get started, let's setup a getPublications function in our utils.js file. Add this piece of code to the bottom of it:

File: ./utils.js

const GET_PUBLICATIONS_QUERY = `
query {
  explorePublications(request: {
    sortCriteria: TOP_COMMENTED,
    publicationTypes: [POST],
    limit: 20
  }) {
    items {
      __typename
      ... on Post {
        ...PostFields
      }
    }
  }
}

fragment ProfileFields on Profile {
  id
  name
  metadata
  handle
  picture {
    ... on NftImage {
      uri
    }
    ... on MediaSet {
      original {
        ...MediaFields
      }
    }
  }
  stats {
    totalComments
    totalMirrors
    totalCollects
  }
}

fragment MediaFields on Media {
  url
}

fragment PublicationStatsFields on PublicationStats {
  totalAmountOfMirrors
  totalAmountOfCollects
  totalAmountOfComments
}

fragment MetadataOutputFields on MetadataOutput {
    content
  media {
    original {
      ...MediaFields
    }
  }
}

fragment PostFields on Post {
  id
  profile {
    ...ProfileFields
  }
  stats {
    ...PublicationStatsFields
  }
  metadata {
    ...MetadataOutputFields
  }
}
  `;

// publications = posts in Lens lingo
export const getPublications = async () => {
  const { data } = await apolloClient.query({
    query: gql(GET_PUBLICATIONS_QUERY),
  });
  return data.explorePublications.items;
};

It's quite a long GraphQL query I know! This is because we are fetching data about a lot of different fields. Let's now use this getPublications function to show some posts in our UI.

In our pages/index.js file, we want to fetch a list of posts using getPublications and display them in our UI nicely.

Add the following piece of code to your pages/index.js file in order to make this happen.

File: ./pages/index.js

import { authenticate, generateChallenge, getPublications } from '../utils';
import { useEffect, useState } from 'react';

export default function Home() {
  const { data } = useAccount();
  const address = data?.address;
  const connected = !!data?.address;
  const { signMessageAsync } = useSignMessage();

  const [posts, setPosts] = useState([]);

  const signIn = async () => {
    try {
      if (!connected) {
        return alert('Please connect your wallet first');
      }
      const challenge = await generateChallenge(address);
      const signature = await signMessageAsync({ message: challenge });
      const accessToken = await authenticate(address, signature);
      console.log({ accessToken });
      window.sessionStorage.setItem('accessToken', accessToken);
    } catch (error) {
      console.error(error);
      alert('Error signing in');
    }
  };

    useEffect(() => {
    getPublications().then(setPosts);
  }, []);

  return (
    <Container paddingY='10'>
      <ConnectButton />

      <Button onClick={signIn} marginTop='2'>
        Login with Lens
      </Button>

      <VStack spacing={4} marginY='10'>
        {posts
          .filter((post) => post.__typename === 'Post') // we need to filter the list to make sure we are not rendering anything other than Posts, like comments and other formats.
          .map((post) => {
            return (
              <VStack
                key={post.id}
                borderWidth='0.7px'
                paddingX='4'
                paddingY='2'
                rounded='md'
                width='full'
                alignItems='left'
                as='a'
                href={`https://lenster.xyz/posts/${post.id}`}
                target='_blank'
                transition='all 0.2s'
                _hover={{
                  shadow: 'md',
                }}
              >
                <HStack>
                  <Avatar src={post.profile.picture.original.url} />
                  <Text fontWeight='bold' justifyContent='left'>
                    {post.profile?.handle || post.profile?.id}
                  </Text>
                </HStack>
                 <Text>{post.metadata?.content}</Text>
                <HStack textColor='gray.400'>
                  <Text>
                    {post.stats?.totalAmountOfComments} comments,
                  </Text>
                  <Text>
                    {post.stats?.totalAmountOfMirrors} mirrors,
                  </Text>
                  <Text>
                    {post.stats?.totalAmountOfCollects} collects
                  </Text>
                </HStack>
              </VStack>
            );
          })}
      </VStack>
    </Container>
  );
}

You should now be seeing a nice list of posts showing up on your timeline. Cool!

https://i.imgur.com/t1Yt8V9.jpg

To add some last few finishing touches, we are going to show some skeletons when the posts are still loading and we are going to hide the login button once someone has signed in.

To add the skeletons, add the following piece of code to your pages/index.js file.(I have omitted the parts of the file which are unaffected)

File: ./pages/index.js

import { Skeleton } from '@chakra-ui/react';

export default function Home() {
    return (
        <Container paddingY='10'>
      <ConnectButton />

      <Button onClick={signIn} marginTop='2'>
        Login with Lens
      </Button>

      <VStack spacing={4} marginY='10'>
        {posts
          .filter((post) => post.__typename === 'Post')
          .map((post) => {
            return (
              ...
            );
          })}

        {/* Skeletons */}
        {posts.length === 0 || !posts ? (
          <VStack>
            {[...Array(10)].map((_, idx) => {
              return <Skeleton key={idx} height='32' width='xl' rounded='md' />;
            })}
          </VStack>
        ) : null}
      </VStack>
    </Container>
    );
}

To hide the Login button when someone is already signed in, we are going to make use of a simple hack. We are going to create a state variable called signedIn and use that variable to show the button conditionally. Let's add the following piece of code to make this happen:

File: ./pages/index.js

export default function Home() {
  const [signedIn, setSignedIn] = useState(false);

  const signIn = async () => {
    try {
      if (!connected) {
        return alert('Please connect your wallet first');
      }
      const challenge = await generateChallenge(data?.address as string);
      const signature = await signMessageAsync({ message: challenge });
      const accessToken = await authenticate(address as string, signature);
      window.sessionStorage.setItem('accessToken', accessToken);

      setSignedIn(true);
    } catch (error) {
      console.error(error);
      alert('Error signing in');
    }
  };

  return (
    <Container paddingY='10'>
      <ConnectButton showBalance={false} />
       {!signedIn && (
        <Button onClick={signIn} marginTop='2'>
          Login with Lens
        </Button>
       )}
            ...

There we have it! We now have a nice-looking, simple React frontend built on top of Lens Protocol in front of us.

https://i.imgur.com/xY230ew.gif

Full Code Repository

What’s Next?

Now that you know how to authenticate users using the Lens API, the possibilities for you are endless. I highly suggest browsing through the Lens API docs and the Lens Showcase for some inspiration. You can build anything from a Hackernews clone to a decentralized YouTube clone. You have the world at your feet!