Giter VIP home page Giter VIP logo

neynar-next's Introduction

🪐 neynar-next GitHub Workflow Status (branch) npm

Important

This project has been superseded by the official Neynar Node.js SDK. It's suggested to use that going forward. Please get in touch if you need help migrating!

Create Farcaster apps with Neynar. Built with Next.js in mind, but works with any React client and JavaScript server.

NOTE: The approach to signers used below results in a new signer being created per app, as the signers are stored in the user's localStorage. This requires users to pay to register each device separately. It's possible to mitigate this issue by having the user authenticate separately (i.e. with SIWE) and then persisting the signer UUID and user ID in your backend so other clients can reuse it. I'm currently exploring how that approach could be incorporated into this library in the future.

Features

This repo is a work in progress, use at your own risk! Currently, the following features are supported:

  • Sign in
  • Get user profile by FID or username
  • Fetch feed by following/channel/FID list
  • Get cast and thread
  • Post casts
  • Cast reactions (like/unlike/recast/unrecast)
  • Get notifications (mentions and replies)
  • Search users
  • Follow/unfollow

Installation

Quick Start

To quickly scaffold a project with a simple feed and cast posting functionality:

npx degit alex-grover/neynar-next/example my-farcaster-app

All you need to do is add your FID, Farcaster mnemonic, and Neynar API key to the project's .env and you'll be up and running!

Manual Installation

npm install neynar-next viem

Usage

This library has 2 parts: a client-side context/provider to manage the signer, and a server class that simplifies making requests to Neynar.

It's necessary to run Neynar API requests through your own server in order to keep your API key secret. However, beyond proxying requests from your frontend, the server implementation is quite lightweight. It's designed to be compatible with the Next.js Edge runtime to minimize any latency to the user.

Sign in

Add the provider:

// app/layout.tsx - similar implementation for pages/_app.tsx

import { PropsWithChildren } from 'react'
import { NeynarProvider } from 'neynar-next'

export default function RootLayout({ children }: PropsWithChildren) {
  return (
    <html lang="en">
      <head />
      <body>
        <NeynarProvider>{children}</NeynarProvider>
      </body>
    </html>
  )
}

Set up the client and add the API to your server:

// lib/neynar.ts

import NeynarClient from 'neynar-next/server'

const neynarClient = new NeynarClient(
  process.env.NEYNAR_API_KEY!,
  BigInt(process.env.FARCASTER_FID!),
  process.env.FARCASTER_MNEMONIC!,
)

export default neynarClient

The client passes a query param of ?signer_uuid=XXX and expects a Signer object back from the server.

`app` directory
// app/api/signer/route.ts

import { NextResponse } from 'next/server'
import { neynarClient } from '@/lib/neynar'

export const runtime = 'edge'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const signerUuid = searchParams.get('signer_uuid')
  if (!signerUuid)
    return new Response('signer_uuid query param is required', { status: 400 })
  const signer = await neynarClient.getSigner(signerUuid)

  return NextResponse.json(signer)
}

export async function POST() {
  const signer = await neynarClient.createSigner()
  return NextResponse.json(signer)
}
`pages` directory
// pages/api/signer.ts

import { NextApiRequest, NextApiResponse } from 'next'
import { neynarClient } from '@/lib/neynar'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse,
) {
  switch (req.method) {
    case 'GET': {
      const signer = await neynarClient.getSigner(req.query.signer_uuid)
      res.status(200).json(signer)
      break
    }
    case 'POST': {
      const signer = await neynarClient.createSigner()
      res.status(201).json(signer)
    }
    default:
      res.status(405).end()
  }
}

It's possible to change the API path via the NeynarProvider api prop, if desired:

Customize API path
// app/layout.tsx

import { PropsWithChildren } from 'react'
import { NeynarProvider } from 'neynar-next'

export default function RootLayout({ children }: PropsWithChildren) {
  return (
    <html lang="en">
      <head />
      <body>
        <NeynarProvider api="/api/neynar/signer">{children}</NeynarProvider>
      </body>
    </html>
  )
}

Then, use the hook in your app:

'use client'

import { useSigner } from 'neynar-next'
import { useCallback } from 'react'

export default function LoginButton() {
  const { signer, isLoading, signIn } = useSigner()

  const handleClick = useCallback(() => void signIn(), [signIn])

  if (isLoading) return 'Loading...'

  switch (signer?.status) {
    case undefined:
      return <button onClick={handleClick}>Sign In</button>
    case 'generated':
      // This should never happen, unless the server fails while registering the signer
      throw new Error('Unregistered signer')
    case 'pending_approval':
      return (
        <>
          {/* See below */}
          <QRCodeModal signer={signer} />
          <button disabled>Loading</button>
        </>
      )
    case 'approved':
      return <div>Signed in as FID {signer?.fid}</div>
    case 'revoked':
      return <button onClick={handleClick}>Revoked. Sign In Again</button>
  }
}

After the user clicks the sign in button, you'll need to render a QR code so they can add the signer from the Warpcast mobile app. You can do this with a package like react-qr-code:

'use client'

import { useSigner } from 'neynar-next'
import QRCode from 'react-qr-code'

export default function QRCodeModal() {
  const { signer } = useSigner()

  if (signer?.status !== 'pending_approval') return null

  return (
    <div className="modal">
      <QRCode value={signer.signer_approval_url} />
    </div>
  )
}
Fetch user

After signing in, you'll likely want to fetch the user's profile so you can display their username and avatar. To do this, we need to create an API route and then fetch the user from the client:

// app/api/users/[fid]/route.ts

type Props = {
  params: {
    fid: string
  }
}

export async function GET(request: Request, { params }: Props) {
  const fid = parseInt(params.fid)
  if (!fid) return new Response('fid is invalid', { status: 400 })

  // You can pass an optional viewer FID to get back the mutual following status as well, for example to display on another user's profile page
  // const { searchParams } = new URL(request.url)
  // const viewer = searchParams.get('viewer')

  const user = await neynarClient.getUserByFid(fid /*, viewer */)
  // Or fetch by username
  // await neynarClient.getUserByUsername('alexgrover.eth' /*, viewer */)
  return NextResponse.json(user)
}

This library is agnostic of your client data fetching solution. The example uses swr, but you can use react-query or plain fetch if you'd like.

'use client'

import { useSigner } from 'neynar-next'
import { type User } from 'neynar-next/server'
import useSWRImmutable from 'swr/immutable'

export default function UserProfile() {
  const { signer } = useSigner()

  const { data } = useSWRImmutable<User, string>(
    signer?.status === 'approved' ? `/api/users/${signer.fid}` : null,
  )

  if (!data) return null

  return <div>{data.username}</div>
}
Fetch feed for user/channel/list of users

Add the API to your server:

// app/api/casts/route.ts

import { NextResponse } from 'next/server'
import { neynarClient } from '@/lib/neynar'

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const fid = parseInt(searchParams.get('fid'))
  if (!fid) return new Response('fid query param is required', { status: 400 })
  const feed = await neynarClient.getFollowingFeed(fid)
  // Or you can get the feed for a channel/specific list of users:
  // const feed = await neynarClient.getChannelFeed(fid)
  // const feed = await neynarClient.getFeedForFids([10259]) // There seems to be a bug on the Neynar end where this fails with more than 1 FID
  return NextResponse.json(feed)
}

Then, hit the API from your client:

'use client'

import { useSigner } from 'neynar-next'
import { FeedResponse, Signer } from 'neynar-next/server'
import { useCallback } from 'react'
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite'

export default function Casts() {
  const { signer, isLoading: signerLoading } = useSigner()
  const { data, isLoading, error, size, setSize } = useSWRInfinite<
    FeedResponse,
    string
  >(getKey(signer))

  const loadMore = useCallback(
    () => setSize((current) => current + 1),
    [setSize],
  )

  if (signerLoading) return 'Loading'
  if (signer?.status !== 'approved') return 'Please sign in to view casts'

  return (
    <>
      {data?.map((page, index) =>
        page.casts.map((cast) => (
          <div key={cast.hash}>{/* render cast */}</div>
        )),
      )}
      {isLoading && 'Loading'}
      {error && <div>{error}</div>}
      <button onClick={loadMore}>Load More</button>
    </>
  )
}

const API_URL = '/api/casts'

function getKey(signer: Signer | null): SWRInfiniteKeyLoader<FeedResponse> {
  return (pageIndex, previousPageData) => {
    if (signer?.status !== 'approved') return null
    const params = new URLSearchParams({ fid: signer.fid.toString() })

    if (pageIndex === 0) return `${API_URL}?${params.toString()}`

    if (previousPageData && !previousPageData.next.cursor) return null

    if (previousPageData?.next.cursor)
      params.set('cursor', previousPageData.next.cursor)
    return `${API_URL}?${params.toString()}`
  }
}
Get cast or thread

Add the API to your server:

// app/api/casts/[hash]/route.ts

type Props = {
  params: {
    hash: string
  }
}

export async function GET(request: Request, { params }: Props) {
  const hash = params.hash
  if (!hash || !hash.startsWith('0x'))
    return new Response('hash is invalid', { status: 400 })

  const { cast } = await neynarClient.getCast('hash', hash)
  // Or fetch by url
  // await neynarClient.getCast('url', 'https://warpcast.com/...')

  // Or fetch thread
  // You can pass an optional viewer FID to get back whether that FID has reacted to the cast already
  // const { searchParams } = new URL(request.url)
  // const viewer = searchParams.get('viewer')
  // await neynarClient.getCastsInThread('0x...' /* , { viewer } */)
  return NextResponse.json(cast)
}

Then, hit the API from your client:

'use client'

import { type Cast } from 'neynar-next/server'
import useSWR from 'swr'

export default function CastDetailPage() {
  const { data } = useSWR<Cast, string>(`/api/casts/0x...`)

  if (!data) return null

  return <div>{data.text}</div>
}
Post or delete a cast

Add the API to your server:

// app/api/casts/route.ts

export async function POST(request: Request) {
  const { searchParams } = new URL(request.url)
  if (!searchParams.get('signerUuid'))
    return new Response('signerUuid query param is required', { status: 400 })

  const data = Object.fromEntries((await request.formData()).entries())
  if (!data.text) const { signerUuid, text } = parseResult.data
  await neynarClient.postCast(
    signerUuid,
    text,
    // { embeds: [{ url: '' }], parent: '' }
  )

  return NextResponse.json({}, { status: 201 })
}

// To delete a cast, use the same approach with `neynarClient.deleteCast(signerUuid, castHash)`

Then, hit the API from your client:

'use client'

import { useSigner } from 'neynar-next'

export default function CastForm() {
  const { signer } = useSigner()

  const handleSubmit = useCallback(
    (event: FormEvent<HTMLFormElement>) => {
      event.preventDefault()

      if (signer?.status !== 'approved') return

      const params = new URLSearchParams({ signerUuid: arg.signer.signer_uuid })
      void fetch(`/api/casts?${params.toString()}`, {
        method: 'POST',
        body: new FormData(event.currentTarget),
      })
    },
    [signer, trigger],
  )

  return (
    <form onSubmit={handleSubmit}>
      <textarea name="text" />
      <button type="submit">Post</button>
    </form>
  )
}
React to cast (like, unlike, recast, undo recast)

Add the API to your server:

// app/api/casts/[hash]/{like,recast}/route.ts

import { NextResponse } from 'next/server'
import neynarClient from '@/lib/neynar'

type Props = {
  params: {
    hash: string
  }
}

export async function POST(request: Request, { params }: Props) {
  const searchParams = new URLSearchParams(request.url)
  await neynarClient.likeCast(searchParams.get('signerUuid'), params.hash)
  // await neynarClient.recastCast(searchParams.get('signerUuid'), params.hash)
  return NextResponse.json({}, { status: 201 })
}

export async function DELETE(request: Request, { params }: Props) {
  const searchParams = new URLSearchParams(request.url)
  await neynarClient.unlikeCast(searchParams.get('signerUuid'), params.hash)
  // await neynarClient.unrecastCast(searchParams.get('signerUuid'), params.hash)
  return NextResponse.json({}, { status: 204 })
}

Then, hit the API from your client:

'use client'

import { useSigner } from 'neynar-next'

type LikeButtonProps = {
  cast: {
    hash: string
  }
}

export default function LikeButton({ cast }: LikeButtonProps) {
  const { signer } = useSigner()

  return (
    <button
      disabled={signer?.status !== 'approved' || isMutating}
      onClick={() =>
        fetch(`/api/casts/${cast.hash}/like?signerUuid=${signer.signer_uuid}`, {
          method: 'POST',
        })
      }
    >
      Like
    </button>
  )
}
Get notifications (mentions and replies)

Add the API to your server:

// app/api/users/[fid]/notifications/route.ts
type Props = {
  params: {
    fid: string
  }
}

export async function GET(request: Request, { params }: Props) {
  const fid = parseInt(params.fid)
  if (!fid) return new Response('fid is invalid', { status: 400 })

  // Optional viewer context and pagination params
  // const { searchParams } = new URL(request.url)
  // const viewer = searchParams.get('viewer')
  // const cursor = searchParams.get('cursor')
  // const limit = searchParams.get('limit')

  const { result } = await neynarClient.getMentionsAndReplies(
    fid /*, { viewer, cursor, limit } */,
  )
  return NextResponse.json(result)
}

Then, hit the API from your client:

'use client'

import { type Notification } from 'neynar-next/server'
import useSWR from 'swr/immutable'

export default function Notifications() {
  const { data } = useSWR<{ notifications: Notification[] }, string>(
    signer?.status === 'approved'
      ? `/api/users/${signer.fid}/notifications`
      : null,
  )

  if (!data) return null

  return (
    <div>
      {data.notifications.map((notification) => (
        <div>{notification.text}</div>
      ))}
    </div>
  )
}

neynar-next's People

Contributors

alex-grover avatar dependabot[bot] avatar github-actions[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

cnrmck sangeetxyz

neynar-next's Issues

🐛 Bug Report: getCastsInThread does not return type Cast[]

Describe the bug
The return type of getCastsInThread is:

Promise<{
    result: {
        casts: Cast[];
    };
}>

However, the actual internal values of result.casts is

{
  "hash": "0x90a9b8eb759fa1bcab4479455e5bb46e8f9fac10",
  "parentHash": "0x7865cc2aa89a96557363c5d6fa95a16282ac5251",
  "parentUrl": null,
  "parentAuthor": {
    "fid": "2588"
  },
  "author": {
    "fid": 2588,
    "custodyAddress": "0x00275fdd516e5a5b9a017e5181f375aaf2751302",
    "username": "nor",
    "displayName": "Connor McCormick 🥝/🫦",
    "pfp": {
      "url": "https://i.imgur.com/1JjZFIB.png"
    },
    "profile": {
      "bio": {
        "text": "The only problem is externality pricing. Follower",
        "mentions": []
      }
    },
    "followerCount": 6459,
    "followingCount": 1590,
    "verifications": [
      "0x4f8c531df3d97c6cd437ac8dfe756975445d1161"
    ],
    "activeStatus": "active"
  },
  "text": "nah\nhttps://warpcast.com/nor/0xb0f47f",
  "timestamp": "2023-10-08T12:30:35.000Z",
  "embeds": [
    {
      "url": "https://warpcast.com/nor/0xb0f47f"
    }
  ],
  "reactions": {
    "count": 0,
    "fids": []
  },
  "recasts": {
    "count": 0,
    "fids": []
  },
  "recasters": [],
  "replies": {
    "count": 2
  },
  "threadHash": null
}

What's expected instead is a structure matching the Cast type, like this:

{
  "hash": "0x6f7792e8e224b6707f2fa16dc71cc618eb98f995",
  "thread_hash": null,
  "parent_hash": null,
  "parent_url": "https://option.app",
  "parent_author": {
    "fid": null
  },
  "author": {
    "fid": 2588,
    "custody_address": "0x00275fdd516e5a5b9a017e5181f375aaf2751302",
    "username": "nor",
    "display_name": "Connor McCormick 🥝/🫦",
    "pfp_url": "https://i.imgur.com/1JjZFIB.png",
    "profile": {
      "bio": {
        "text": "The only problem is externality pricing. Follower",
        "mentions": []
      }
    },
    "follower_count": 6459,
    "following_count": 1590,
    "verifications": [
      "0x4f8c531df3d97c6cd437ac8dfe756975445d1161"
    ],
    "active_status": "active"
  },
  "text": "It's impossible to build AGI.",
  "timestamp": "2023-10-12T13:45:19.000Z",
  "embeds": [],
  "reactions": {
    "likes": [],
    "recasts": []
  },
  "replies": {
    "count": 0
  }
}

💡Feature Request: Consistent User Types

Cast.author has a bespoke definition, so the response from the getUser endpoint returns a different User type than the author field in the getCast endpoint, it would be easier to interface with if Cast.author was the same User definition as the standard User type

I.e. switch from:

export type Cast = {
  hash: Hash
  thread_hash: Hash | null
  parent_hash: Hash | null
  parent_url: string | null
  parent_author: {
    fid: number | null
  }
  author: {
    fid: number
    username: string
    display_name: string
    pfp_url: string
    profile: {
      bio: {
        text: string
      }
    }
    follower_count: number
    following_count: number
    verifications: Address[]
    active_status: 'active' | 'inactive'
  }
  text: string
  timestamp: string
  embeds: Embed[]
  reactions: {
    likes: Like[]
    recasts: Recast[]
  }
  replies: {
    count: number
  }
}

To

export type Cast = {
  hash: Hash
  thread_hash: Hash | null
  parent_hash: Hash | null
  parent_url: string | null
  parent_author: {
    fid: number | null
  }
  author: User
  text: string
  timestamp: string
  embeds: Embed[]
  reactions: {
    likes: Like[]
    recasts: Recast[]
  }
  replies: {
    count: number
  }
}

🐛 Bug Report: QR Code is unscannable

Login flow produces a non-functional qr code

To reproduce:

  1. Launch the app with a new user
  2. Click login
  3. Try scanning the qr code

Here's what it produced for me:
image

Should be rendering this url:
farcaster://signed-key-request?token=0xa875dc332db8c1a97d9eb4cd

💡Feature Request: Missing client methods

There are several client methods that are missing still.

So far, the methods covered include:

  • createSigner
  • deleteCast
  • getChannelFeed
  • getFeedForFids
  • getFollowingFeed
  • getSigner
  • getUserByFid
  • getUserByUsername
  • likeCast
  • postCast
  • recastCast
  • unlikeCast
  • unrecastCast

However, there are several critical methods that are missing. These include at least:

  • getCastInformation (by url or by hash) docs
  • getThread (by url or by hash) docs
  • getNotifications (by user fid) docs

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.