Skip to content

Commit

Permalink
swap Privy for Neynar on frontend, with backend debugging
Browse files Browse the repository at this point in the history
  • Loading branch information
artlu99 committed Nov 3, 2024
1 parent 94d9957 commit 5dcbad4
Show file tree
Hide file tree
Showing 37 changed files with 4,681 additions and 280 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
REACT_APP_READ_TOKEN=XXXXXXXX-XXXX-XXXXX
REACT_APP_DEFAULT_FID=391262;
REACT_APP_NEYNAR_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
REACT_APP_PRIVY_APP_ID=xxxxxxxxxxxxxxxxxxxxxxxxx
REACT_APP_PINATA_GATEWAY=https://gateway.pinata.cloud
REACT_APP_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
REACT_APP_PUBLIC_POSTHOG_KEY=xxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Expand All @@ -12,4 +13,5 @@ DECENTBOOKMARKS_TOKEN=private-basic-auth-token
AIRSTACK_API_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
BOT_OR_NOT_API=https://sample.api.bot-or-not.xyz/api/botornot/data
SASSYHASH_API=https://whistles.artlu.xyz/graphql
WHISTLES_BEARER_TOKEN=<secret-bearer-token>
WHISTLES_BEARER_TOKEN=<secret-bearer-token>
PRIVY_APP_SECRET=XXXXXXXXXXXXXXXXXXXX
54 changes: 27 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,51 @@
# BCBHShow Lite Client 🌟

#### Overview
## Overview

- front end is a responsive React 18 app
- Typescript
- Redux state management for persistent stores
- Zustand state management for in-app state
- optional, recommended PWA
- `Neynar` handles auth entirely in the front-end (state-light client)
- Ant Design v4 principles (i.e. `less` CSS, choice to deal with it later)
- stripped and rebuilt from a MIT-licensed template
- developed by a Polish web design firm
- had not been upgraded to current versions of React, Ant, etc.
- Typescript
- Redux state management for persistent stores
- Zustand state management for in-app state
- optional, recommended PWA
- `Privy` handles auth as well as free Farcaster signers
- Ant Design v4 principles (i.e. `less` CSS, choice to deal with it later)
- stripped and rebuilt from a MIT-licensed template
- developed by a Polish web design firm
- had not been upgraded to current versions of React, Ant, etc.
- back end is a collection of independent, stateless CF Pages Functions
- each one calls `fetch` to various providers
- `Warpcast` APIs
- `Neynar` APIs wrapped by `Pinata`
- `far.quest` APIs
- `Hubble` APIs via `Airstack` (designed to be swapped)
- `Decent Bookmarks` and `FCAN` by `@artlu`
- `bot-or-not` by `@sayangel`
- bindings to `Upstash Redis` via https REST API for caching
- the DX is unfamiliar to devs familiar with `Next.js`. study `src/api/mocks`
- each one calls `fetch` to various providers
- `Warpcast` APIs
- `Neynar` APIs wrapped by `Pinata`
- `far.quest` APIs
- `Hubble` APIs via `Airstack` (designed to be swapped)
- `Decent Bookmarks` and `FCAN` by `@artlu`
- `bot-or-not` by `@sayangel`
- bindings to `Upstash Redis` via https REST API for caching
- the DX is unfamiliar to devs familiar with `Next.js`. study `src/api/mocks`
- `React Query` caches many of the backend calls in the front end
- mocked data available for optionally-offline front-end development
- Hono microservice for unfurling URLs
- in a different MIT-licensed repo [here](https://github.com/artlu99/unfurl))
- runs `cheerio` underneath
- heavily inspired by Pinata Steve
- in a different MIT-licensed repo [here](https://github.com/artlu99/unfurl))
- runs `cheerio` underneath
- heavily inspired by Pinata Steve

#### Installation
### Installation

run

```
```sh
yarn
```

#### development (front-end only)
### development (front-end only)

uncomment the imports of mocked data in `src/api/mocks/mockornot.ts` and run

```
```sh
yarn start
```

#### deployment
### deployment

set the `base` in `vite.config.js` to the URL of the deployed site (including '`https://`').

Expand Down
2 changes: 2 additions & 0 deletions functions/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ export interface Env {
REACT_APP_DEFAULT_FID: number;
REACT_APP_PUBLIC_POSTHOG_HOST: string;
REACT_APP_PUBLIC_POSTHOG_KEY: string;
REACT_APP_PRIVY_APP_ID: string;
PRIVY_APP_SECRET: string;
D1: D1Database;
}
23 changes: 20 additions & 3 deletions functions/getSassyHashes/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PrivyClient } from '@privy-io/server-auth';
import { Client, fetchExchange, gql } from '@urql/core';

import { Env } from '../common';
Expand All @@ -12,10 +13,25 @@ interface SassyHashGraphQLResponse {
getTextByCastHash: SassyHash;
}
interface SassyHashRequest {
fid: string;
privyAuthToken: string;
castHash: string;
}

const getFid = async (privyAuthToken: string, env: Env): Promise<number> => {
const privy = new PrivyClient(env.REACT_APP_PRIVY_APP_ID, env.PRIVY_APP_SECRET);

try {
const user = await privy.getUser({ idToken: privyAuthToken });
console.log('Privy User:', user);

const { fid } = user.farcaster;
return fid;
} catch (error) {
console.error(`Token verification failed with error ${error}.`);
throw new Error('Failed to fetch Farcaster FID');
}
};

const fetchSassyHashExpensiveApi = async (viewerFid: number, castHash: string, env: Env) => {
const client = new Client({
url: env.SASSYHASH_API,
Expand Down Expand Up @@ -47,9 +63,10 @@ const fetchSassyHashExpensiveApi = async (viewerFid: number, castHash: string, e
export const onRequestPost: PagesFunction<Env> = async (context) => {
const { env, request } = context;
const js = (await request.json()) as SassyHashRequest;
const { fid, castHash } = js;
const { privyAuthToken, castHash } = js;

const sassyHashResponses = await fetchSassyHashExpensiveApi(parseInt(fid), castHash, env);
const fid = await getFid(privyAuthToken, env);
const sassyHashResponses = await fetchSassyHashExpensiveApi(fid, castHash, env);

return new Response(JSON.stringify(sassyHashResponses));
};
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.png" />
<link rel="stylesheet" href="/themes/main.css" type = "text/css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="BCBHShow Lite Client 🌟 with Sign In With Neynar free signers" />
<meta name="keywords" content="farcaster channel frames actions bcbhshow beavchris barthead ryan gosling unlonely base zora paragraph drakula seemore frog neynar pinata airstack" />
<meta name="description" content="BCBHShow Lite Client 🌟 with Privy free signers" />
<meta name="keywords" content="farcaster channel frames actions bcbhshow beavchris barthead ryan gosling unlonely base zora paragraph drakula seemore frog privy neynar pinata airstack" />
<meta name="themecolor" content="#ffffff" />
<meta itemprop="title" content="🌟 BCBHShow Lite Client" />
<meta itemprop="description" content="Farcaster lite client for the /bcbhshow channel" />
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"@fontsource/noto-sans": "^5.0.22",
"@lit/react": "^1.0.5",
"@neynar/nodejs-sdk": "^1.31.0",
"@neynar/react": "^0.3.1",
"@privy-io/react-auth": "latest",
"@privy-io/server-auth": "^1.15.3",
"@reduxjs/toolkit": "^2.2.5",
"@standard-crypto/farcaster-js": "^7.4.0",
"@tanstack/query-sync-storage-persister": "^5.45.0",
"@tanstack/react-query": "^5.45.0",
"@tanstack/react-query-persist-client": "^5.45.0",
Expand Down
28 changes: 8 additions & 20 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ConfigProvider } from 'antd';
import { posthog } from 'posthog-js';
import { HelmetProvider } from 'react-helmet-async';

import { NeynarContextProvider, Theme } from '@neynar/react';
import { PrivyProvider } from '@privy-io/react-auth';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
Expand All @@ -24,7 +23,7 @@ import jaJP from 'antd/lib/locale/ja_JP';

import GlobalStyle from './styles/GlobalStyle';

const clientId = import.meta.env.REACT_APP_NEYNAR_CLIENT_ID;
const appId = import.meta.env.REACT_APP_PRIVY_APP_ID;

const App: React.FC = () => {
const { language } = useLanguage();
Expand Down Expand Up @@ -60,22 +59,11 @@ const App: React.FC = () => {
<>
<meta name="theme-color" content={themeObject[theme].primary} />
<GlobalStyle />
<NeynarContextProvider
settings={{
clientId,
defaultTheme: Theme.Light,
eventsCallbacks: {
onAuthSuccess: () => {
posthog.capture('user logged in', {
method: 'siwn',
});
},
onSignout() {
posthog.capture('user logged out', {
method: 'siwn',
});
},
},
<PrivyProvider
appId={appId}
config={{
loginMethods: ['farcaster'],
embeddedWallets: { createOnLogin: 'users-without-wallets' },
}}
>
<QueryClientProvider client={queryClient}>
Expand All @@ -87,7 +75,7 @@ const App: React.FC = () => {
</ConfigProvider>
</HelmetProvider>
</QueryClientProvider>
</NeynarContextProvider>
</PrivyProvider>
</>
);
};
Expand Down
9 changes: 6 additions & 3 deletions src/api/channelFeed.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,24 @@ export interface PagedCronFeed {
}

interface ChannelFeedRequest {
fid: number;
getAccessToken: () => Promise<string | null>;
channel?: ChannelObject;
pageToken?: string;
following: number[];
}

export const getEnhancedChannelFeed = async (channelFeedRequestPayload: ChannelFeedRequest): Promise<PagedCronFeed> => {
const { channel, fid, pageToken, following } = channelFeedRequestPayload;
const { channel, getAccessToken, pageToken, following } = channelFeedRequestPayload;
if (!channel) return { casts: [] };

const privyAuthToken = await getAccessToken();
const cronFeed = await getCronFeed({ channelId: channel.id, pageSize: CHANNEL_FEED_PAGESIZE, pageToken });
const seenFids = sift(cronFeed.casts.map((cast) => cast.author.fid).filter((fid) => fid !== null));
const seenSassyHashes = unique(sift(cronFeed.casts.map((cast) => (isSassy(cast.text) ? cast.hash : null))));
const botOrNotResponse = await getBotOrNot({ fids: seenFids ?? [] });
const sassyHashResponses = await Promise.all(seenSassyHashes.map((sh) => getSassyHash({ fid, castHash: sh })));
const sassyHashResponses = privyAuthToken
? await Promise.all(seenSassyHashes.map((sh) => getSassyHash({ privyAuthToken, castHash: sh })))
: [];

return {
...cronFeed,
Expand Down
8 changes: 6 additions & 2 deletions src/api/followingFeed.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,23 @@ export const getFollowingFeed = (followingFeedRequestPayload: FollowingFeedReque

interface EnhancedFollowingFeedRequest {
fid: number;
getAccessToken: () => Promise<string | null>;
pageToken?: string;
allChannels: ChannelObject[];
}
export const getEnhancedFollowingFeed = async (
homeFeedRequestPayload: EnhancedFollowingFeedRequest,
): Promise<PagedCronFeed> => {
const { fid, pageToken, allChannels } = homeFeedRequestPayload;
const { fid, getAccessToken, pageToken, allChannels } = homeFeedRequestPayload;

const privyAuthToken = await getAccessToken();
const cronFeed = await getFollowingFeed({ fid: fid, pageSize: FOLLOWING_FEED_PAGESIZE, pageToken });
const seenFids = sift(cronFeed.casts.map((cast) => cast.author.fid).filter((fid) => fid !== null));
const seenSassyHashes = unique(sift(cronFeed.casts.map((cast) => (isSassy(cast.text) ? cast.hash : null))));
const botOrNotResponse = await getBotOrNot({ fids: seenFids ?? [] });
const sassyHashResponses = await Promise.all(seenSassyHashes.map((sh) => getSassyHash({ fid, castHash: sh })));
const sassyHashResponses = privyAuthToken
? await Promise.all(seenSassyHashes.map((sh) => getSassyHash({ privyAuthToken, castHash: sh })))
: [];

return {
...cronFeed,
Expand Down
8 changes: 6 additions & 2 deletions src/api/forYouFeed.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,24 @@ export const getNeynarOpenrankForYouFeed = (forYouFeedRequestPayload: ForYouFeed

interface EnhancedForYouFeedRequest {
fid: number;
getAccessToken: () => Promise<string | null>;
cursor?: string;
following: number[];
allChannels: ChannelObject[];
}
export const getEnhancedForYouFeed = async (
homeFeedRequestPayload: EnhancedForYouFeedRequest,
): Promise<PagedCronFeed> => {
const { fid, cursor, following, allChannels } = homeFeedRequestPayload;
const { fid, getAccessToken, cursor, following, allChannels } = homeFeedRequestPayload;

const privyAuthToken = await getAccessToken();
const forYouFeed = await getNeynarOpenrankForYouFeed({ fid: fid, limit: FORYOU_FEED_PAGESIZE, cursor });
const seenFids = sift(forYouFeed.casts.map((cast) => cast.author.fid).filter((fid) => fid !== null));
const seenSassyHashes = unique(sift(forYouFeed.casts.map((cast) => (isSassy(cast.text) ? cast.hash : null))));
const botOrNotResponse = await getBotOrNot({ fids: seenFids ?? [] });
const sassyHashResponses = await Promise.all(seenSassyHashes.map((sh) => getSassyHash({ fid, castHash: sh })));
const sassyHashResponses = privyAuthToken
? await Promise.all(seenSassyHashes.map((sh) => getSassyHash({ privyAuthToken, castHash: sh })))
: [];

return {
...forYouFeed,
Expand Down
4 changes: 2 additions & 2 deletions src/api/reactionOnHash.api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { httpApi } from '@app/api/http.api';

interface ReactionRequest {
signerId: string;
hash: string;
privyAuthToken: string;
target: { fid: number; hash: string };
reactionType: 'like' | 'recast' | 'unlike' | 'unrecast';
}

Expand Down
2 changes: 1 addition & 1 deletion src/api/sassyHash.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface SassyHashResponse {
data: SassyHash;
}
interface SassyHashRequest {
fid: number;
privyAuthToken: string;
castHash: string;
}

Expand Down
10 changes: 5 additions & 5 deletions src/auth/fids.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { INeynarAuthenticatedUser } from '@neynar/react/dist/types/common';
import { User } from '@privy-io/react-auth';

export const getFidWithFallback = (user: INeynarAuthenticatedUser | null): number => {
return user?.fid ?? (import.meta.env.REACT_APP_DEFAULT_FID as number);
export const getFidWithFallback = (user: User | null): number => {
return user?.farcaster?.fid ?? (import.meta.env.REACT_APP_DEFAULT_FID as number);
};

export const getStrictFid = (user: INeynarAuthenticatedUser | null): number | undefined => {
return user?.fid;
export const getStrictFid = (user: User | null): number | undefined => {
return user?.farcaster?.fid ?? undefined;
};
4 changes: 2 additions & 2 deletions src/components/apps/bookmarks/Bookmarks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getFidWithFallback } from '@app/auth/fids';
import { BookmarkCard } from '@app/components/apps/bookmarks/BookmarkCard';
import { BaseEmpty } from '@app/components/common/BaseEmpty/BaseEmpty';
import { BaseFeed } from '@app/components/common/BaseFeed/BaseFeed';
import { useNeynarContext } from '@neynar/react';
import { usePrivy } from '@privy-io/react-auth';
import { useEffect, useState } from 'react';

interface DecentBookmarkResponse {
Expand All @@ -14,7 +14,7 @@ export const Bookmarks: React.FC = () => {
const [hasMore] = useState<boolean>(false);
const [loaded, setLoaded] = useState<boolean>(false);

const { user } = useNeynarContext();
const { user } = usePrivy();
const fid = getFidWithFallback(user);

useEffect(() => {
Expand Down
13 changes: 7 additions & 6 deletions src/components/apps/cast/EmbedHash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function EmbedHash({ hash, containerLevel }: { hash: string; containerLev
return (
<Cast
level={containerLevel + 1}
key={`${cast.hash}-{containerLevel + 1}-${0}`}
castHash={cast.hash}
title={' '}
date={cast.timestamp}
Expand All @@ -28,12 +29,12 @@ export function EmbedHash({ hash, containerLevel }: { hash: string; containerLev
avatar={cast.author.pfp_url}
parentHash={cast.parent_hash}
threadHash={cast.thread_hash}
parentUrl={cast.parent_url}
replies={cast.replies.count}
recasts={cast.reactions.recasts_count}
likes={cast.reactions.likes_count}
recastooors={cast.reactions.recasts.map((r) => r.fid)}
likooors={cast.reactions.likes.map((l) => l.fid)}
parentUrl={cast.parent_url ?? undefined}
replies={cast.replies?.count ?? 0}
recasts={cast.reactions?.recasts_count ?? 0}
likes={cast.reactions?.likes_count ?? 0}
recastooors={cast.reactions?.recasts.map((r) => r.fid) ?? []}
likooors={cast.reactions?.likes.map((l) => l.fid) ?? []}
tags={[]}
/>
);
Expand Down
Loading

0 comments on commit 5dcbad4

Please sign in to comment.