From 31c0662db51a94f441e82e7e9e5ddecf0e6d6210 Mon Sep 17 00:00:00 2001 From: shixuewen Date: Sat, 2 Mar 2024 13:52:56 +0800 Subject: [PATCH 1/7] fix: cast may be empty --- apps/u3/src/container/Explore.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/u3/src/container/Explore.tsx b/apps/u3/src/container/Explore.tsx index e58f2ece..a901f34a 100644 --- a/apps/u3/src/container/Explore.tsx +++ b/apps/u3/src/container/Explore.tsx @@ -74,7 +74,9 @@ export default function Explore() { } }); setHotPosts({ - posts: casts.splice(0, 6), + posts: casts + .filter((item) => item?.platform && item?.data) + .splice(0, 6), farcasterUserData: temp, isLoading: false, }); From 4927f69cf8494d62bfd37b3bab1ac948764fa17e Mon Sep 17 00:00:00 2001 From: ttang Date: Tue, 5 Mar 2024 15:42:14 +0800 Subject: [PATCH 2/7] feat: frame and og --- apps/u3/package.json | 1 + apps/u3/src/components/social/Embed.tsx | 293 +----------------- apps/u3/src/components/social/EmbedFrame.tsx | 242 +++++++++++++++ .../components/social/EmbedFrameWebsite.tsx | 57 ++++ apps/u3/src/components/social/EmbedOG.tsx | 46 +++ .../src/components/social/farcaster/FCast.tsx | 2 + apps/u3/src/services/social/api/farcaster.ts | 24 +- apps/u3/src/services/social/types/index.ts | 15 + .../src/utils/social/farcaster/getEmbeds.ts | 22 +- 9 files changed, 419 insertions(+), 283 deletions(-) create mode 100644 apps/u3/src/components/social/EmbedFrame.tsx create mode 100644 apps/u3/src/components/social/EmbedFrameWebsite.tsx create mode 100644 apps/u3/src/components/social/EmbedOG.tsx diff --git a/apps/u3/package.json b/apps/u3/package.json index 2ffb44d8..73afc0b2 100644 --- a/apps/u3/package.json +++ b/apps/u3/package.json @@ -57,6 +57,7 @@ "dompurify": "^2.3.10", "ethers": "^5.7.2", "formik": "^2.2.9", + "frames.js": "^0.7.1", "html2canvas-strengthen": "0.0.1", "http-proxy-middleware": "^2.0.6", "immer": "^10.0.3", diff --git a/apps/u3/src/components/social/Embed.tsx b/apps/u3/src/components/social/Embed.tsx index e6031446..0ee10e69 100644 --- a/apps/u3/src/components/social/Embed.tsx +++ b/apps/u3/src/components/social/Embed.tsx @@ -2,44 +2,24 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ComponentPropsWithRef, - useCallback, useEffect, useMemo, useRef, useState, } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - CastId, - Message, - UserDataType, - makeFrameAction, -} from '@farcaster/hub-web'; +import { UserDataType } from '@farcaster/hub-web'; import dayjs from 'dayjs'; -import { toHex } from 'viem'; -import { toast } from 'react-toastify'; -import { Cross2Icon, CaretLeftIcon } from '@radix-ui/react-icons'; import { FarCast, FarCastEmbedMeta, FarCastEmbedMetaCast, } from '../../services/social/types'; import { PostCardEmbedWrapper, PostCardImgWrapper } from './PostCard'; -import { - getFarcasterEmbedCast, - getFarcasterEmbedMetadata, - postFrameActionApi, - postFrameActionRedirectApi, -} from '../../services/social/api/farcaster'; +import { getFarcasterEmbedCast } from '../../services/social/api/farcaster'; import ModalImg from './ModalImg'; import U3ZoraMinter from './farcaster/U3ZoraMinter'; import LinkModal from '../news/links/LinkModal'; -import ColorButton from '../common/button/ColorButton'; -import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; -import { FARCASTER_NETWORK } from '@/constants/farcaster'; -import useFarcasterCastId from '@/hooks/social/farcaster/useFarcasterCastId'; -import ModalContainerFixed from '../common/modal/ModalContainerFixed'; -import { cn } from '@/lib/utils'; +import EmbedFrameWebsite from './EmbedFrameWebsite'; const ValidFrameButtonValue = [ [0, 0, 0, 0].join(''), @@ -51,12 +31,14 @@ const ValidFrameButtonValue = [ export default function Embed({ embedImgs, + embedVideos, embedWebpages, embedCasts, cast, embedCastClick, }: { embedImgs: { url: string }[]; + embedVideos: { url: string }[]; embedWebpages: { url: string }[]; embedCasts: { castId: { fid: number; hash: string } }[]; cast: FarCast; @@ -66,26 +48,11 @@ export default function Embed({ ) => void; }) { const viewRef = useRef(null); - const [metadata, setMetadata] = useState([]); const [metadataCasts, setMetadataCasts] = useState( [] ); const [modalImgIdx, setModalImgIdx] = useState(-1); - const getEmbedWebpagesMetadata = async () => { - const urls = embedWebpages.map((embed) => embed.url); - if (urls.length === 0) return; - try { - const res = await getFarcasterEmbedMetadata([urls[0]]); - const { metadata: respMetadata } = res.data.data; - const data = respMetadata.flatMap((m) => (m ? [m] : [])); - setMetadata(data); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - } - }; - const getEmbedCastsMetadata = async () => { const castIds = embedCasts.map((embed) => embed.castId); if (castIds.length === 0) return; @@ -104,7 +71,6 @@ export default function Embed({ if (!viewRef.current) return; const observer = new IntersectionObserver(([entry]) => { if (entry.isIntersecting) { - getEmbedWebpagesMetadata(); getEmbedCastsMetadata(); observer.disconnect(); } @@ -120,7 +86,8 @@ export default function Embed({ if ( embedImgs.length === 0 && embedWebpages.length === 0 && - embedCasts.length === 0 + embedCasts.length === 0 && + embedVideos.length === 0 ) { return null; } @@ -152,7 +119,8 @@ export default function Embed({ /> )} -
+ {/* {embedVideos */} +
{[...metadataCasts].map((item) => { if (item.cast === undefined) return null; const castHex = Buffer.from(item.cast.hash.data).toString('hex'); @@ -167,252 +135,15 @@ export default function Embed({ /> ); })} - {[...metadata].map((item: FarCastEmbedMeta, idx) => { - if (item.collection) { - return ( - - ); - } - if (checkFarcastFrameValid(item)) { - return ( - - ); - } - return ( - - ); + + {embedWebpages.map((item, idx) => { + return ; })}
); } -function EmbedCastFrame({ - data, - cast, -}: { - data: FarCastEmbedMeta; - cast: FarCast; -}) { - const castId: CastId = useFarcasterCastId({ cast }); - const { encryptedSigner, isConnected, currFid } = useFarcasterCtx(); - - const [frameText, setFrameText] = useState(''); - const [frameRedirect, setFrameRedirect] = useState(''); - const [frameData, setFrameData] = useState(data); - - const postFrameAction = useCallback( - async (index: number) => { - if (!castId) { - console.error('no castId'); - toast.error('no castId'); - return; - } - if (!encryptedSigner || !currFid) { - console.error('no encryptedSigner'); - toast.error('no encryptedSigner'); - return; - } - const trustedDataResult = await makeFrameAction( - { - url: Buffer.from(frameData.url || ''), - buttonIndex: index, - castId, - inputText: Buffer.from(frameText), - state: Buffer.from(''), - }, - { - fid: currFid, - network: FARCASTER_NETWORK, - }, - encryptedSigner - ); - if (trustedDataResult.isErr()) { - throw new Error(trustedDataResult.error.message); - } - - const trustedDataValue = trustedDataResult.value; - const untrustedData = { - fid: currFid, - url: frameData.url || '', - messageHash: toHex(trustedDataValue.hash), - network: FARCASTER_NETWORK, - buttonIndex: index, - inputText: frameText, - castId: { - fid: castId.fid, - hash: toHex(castId.hash), - }, - state: '', - }; - const trustedData = { - messageBytes: Buffer.from( - Message.encode(trustedDataValue).finish() - ).toString('hex'), - }; - const postData = { - actionUrl: frameData.fcFramePostUrl, - untrustedData, - trustedData, - }; - const buttonAction = frameData[`fcFrameButton${index}Action`] || 'post'; - console.log('buttonAction', buttonAction); - if (buttonAction === 'post') { - const resp = await postFrameActionApi(postData); - if (resp.data.code !== 0) { - toast.error(resp.data.msg); - return; - } - setFrameData((prev) => ({ - url: prev.url, - ...resp.data.data?.metadata, - })); - } else if (buttonAction === 'post_redirect') { - const resp = await postFrameActionRedirectApi(postData); - if (resp.data.code !== 0) { - toast.error(resp.data.msg); - return; - } - setFrameRedirect(resp.data.data?.redirectUrl || ''); - } else { - const resp = await postFrameActionApi(postData); - if (resp.data.code !== 0) { - toast.error(resp.data.msg); - return; - } - setFrameData((prev) => ({ - url: prev.url, - ...resp.data.data?.metadata, - })); - } - }, - [frameData, currFid, encryptedSigner, castId, frameText] - ); - return ( - <> -
-
- -
- {frameData.fcFrameInputText && ( -
- { - setFrameText(e.target.value); - }} - /> -
- )} - {isConnected && ( -
- {[ - frameData.fcFrameButton1, - frameData.fcFrameButton2, - frameData.fcFrameButton3, - frameData.fcFrameButton4, - ].map((item, idx) => { - if (!item) return null; - return ( - { - e.stopPropagation(); - postFrameAction(idx + 1); - }} - > - {item} - - ); - })} -
- )} -
- {frameRedirect && ( - { - setFrameRedirect(''); - }} - /> - )} - - ); -} - -function EmbedCastFrameRedirect({ - url, - resetUrl, -}: { - url: string; - resetUrl: () => void; -}) { - return ( - { - resetUrl(); - }} - className="w-full md:w-[420px]" - > -
{ - e.stopPropagation(); - }} - > -
-

⚠️ Leaving u3

- -
-

- You are about to leave u3, please connect your wallet carefully and - take care of your funds. -

-
- - -
-
-
- ); -} - function EmbedCast({ data, ...props diff --git a/apps/u3/src/components/social/EmbedFrame.tsx b/apps/u3/src/components/social/EmbedFrame.tsx new file mode 100644 index 00000000..7eb884ac --- /dev/null +++ b/apps/u3/src/components/social/EmbedFrame.tsx @@ -0,0 +1,242 @@ +/* eslint-disable react/no-array-index-key */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useCallback, useState } from 'react'; +import { Frame } from 'frames.js'; +import { CastId, Message, makeFrameAction } from '@farcaster/hub-web'; +import { toHex } from 'viem'; +import { toast } from 'react-toastify'; +import { Cross2Icon, CaretLeftIcon } from '@radix-ui/react-icons'; +import { FarCast } from '../../services/social/types'; + +import ColorButton from '../common/button/ColorButton'; +import { useFarcasterCtx } from '@/contexts/social/FarcasterCtx'; +import { FARCASTER_NETWORK } from '@/constants/farcaster'; +import useFarcasterCastId from '@/hooks/social/farcaster/useFarcasterCastId'; +import ModalContainerFixed from '../common/modal/ModalContainerFixed'; +import { cn } from '@/lib/utils'; +import { + postFrameActionApi, + postFrameActionRedirectApi, +} from '@/services/social/api/farcaster'; + +export default function EmbedCastFrame({ + url, + data, + cast, +}: { + url: string; + data: Frame; + cast: FarCast; +}) { + const castId: CastId = useFarcasterCastId({ cast }); + const { encryptedSigner, isConnected, currFid } = useFarcasterCtx(); + + const [frameText, setFrameText] = useState(''); + const [frameRedirect, setFrameRedirect] = useState(''); + const [frameData, setFrameData] = useState(data); + + const postFrameAction = useCallback( + async (index: number, action: string, target?: string) => { + if (action === 'link') { + setFrameRedirect(url); + return; + } + if (action === 'mint') { + // website mint + setFrameRedirect(url); + return; + } + if (!castId) { + console.error('no castId'); + toast.error('cast is required'); + return; + } + if (!encryptedSigner || !currFid) { + console.error('no encryptedSigner'); + toast.error('farcaster login is required'); + return; + } + const trustedDataResult = await makeFrameAction( + { + url: Buffer.from(url), + buttonIndex: index, + castId, + inputText: Buffer.from(frameText), + state: Buffer.from(''), + }, + { + fid: currFid, + network: FARCASTER_NETWORK, + }, + encryptedSigner + ); + if (trustedDataResult.isErr()) { + throw new Error(trustedDataResult.error.message); + } + + const trustedDataValue = trustedDataResult.value; + const untrustedData = { + fid: currFid, + url, + messageHash: toHex(trustedDataValue.hash), + network: FARCASTER_NETWORK, + buttonIndex: index, + inputText: frameText, + castId: { + fid: castId.fid, + hash: toHex(castId.hash), + }, + state: '', + }; + const trustedData = { + messageBytes: Buffer.from( + Message.encode(trustedDataValue).finish() + ).toString('hex'), + }; + const postData = { + actionUrl: frameData.postUrl, + untrustedData, + trustedData, + }; + + if (action === 'post_redirect') { + const resp = await postFrameActionRedirectApi(postData); + if (resp.data.code !== 0) { + toast.error(resp.data.msg); + return; + } + setFrameRedirect(resp.data.data?.redirectUrl || ''); + } else { + const resp = await postFrameActionApi(postData); + if (resp.data.code !== 0) { + toast.error(resp.data.msg); + return; + } + const { frame } = resp.data.data; + setFrameData(frame); + } + }, + [frameData, currFid, encryptedSigner, castId, frameText] + ); + return ( + <> +
{ + e.stopPropagation(); + }} + > +
+ +
+ {frameData.inputText && ( +
+ { + setFrameText(e.target.value); + }} + /> +
+ )} + {isConnected && frameData.buttons.length && ( +
+ {frameData.buttons.map((item, idx) => { + if (!item) return null; + return ( + { + e.stopPropagation(); + console.log( + 'postFrameAction', + idx, + item.action, + item.target + ); + postFrameAction(idx + 1, item.action, item.target); + }} + > + {item.label} + + ); + })} +
+ )} +
+ {frameRedirect && ( + { + setFrameRedirect(''); + }} + /> + )} + + ); +} + +function EmbedCastFrameRedirect({ + url, + resetUrl, +}: { + url: string; + resetUrl: () => void; +}) { + return ( + { + resetUrl(); + }} + className="w-full md:w-[420px]" + > +
{ + e.stopPropagation(); + }} + > +
+

⚠️ Leaving u3

+ +
+

+ You are about to leave u3, please connect your wallet carefully and + take care of your funds. +

+
+ + +
+
+
+ ); +} diff --git a/apps/u3/src/components/social/EmbedFrameWebsite.tsx b/apps/u3/src/components/social/EmbedFrameWebsite.tsx new file mode 100644 index 00000000..e50e9a64 --- /dev/null +++ b/apps/u3/src/components/social/EmbedFrameWebsite.tsx @@ -0,0 +1,57 @@ +import { useEffect, useRef, useState } from 'react'; +import { Frame } from 'frames.js'; +import { FarCast } from '@/services/social/types'; +import { getFarcasterEmbedMetadataV2 } from '../../services/social/api/farcaster'; +import EmbedCastFrame from './EmbedFrame'; +import EmbedOG from './EmbedOG'; + +export default function EmbedFrameWebsite({ + url, + cast, +}: { + url: string; + cast: FarCast; +}) { + const viewRef = useRef(null); + const [frame, setFrame] = useState(); + const [og, setOG] = useState(); + const getEmbedWebpagesMetadata = async () => { + try { + const res = await getFarcasterEmbedMetadataV2([url]); + const { metadata: respMetadata } = res.data.data; + if (!respMetadata || !respMetadata[0]) return; + + const { ogData, frame: frameData } = respMetadata[0]; + if (frameData && frameData.version) { + setFrame(frameData); + } else { + setOG(ogData); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + }; + useEffect(() => { + if (!viewRef.current) return; + const observer = new IntersectionObserver(([entry]) => { + if (entry.isIntersecting) { + getEmbedWebpagesMetadata(); + observer.disconnect(); + } + }); + + observer.observe(viewRef.current); + // eslint-disable-next-line consistent-return + return () => { + observer.disconnect(); + }; + }, [viewRef]); + + return ( +
+ {frame && } + {og && } +
+ ); +} diff --git a/apps/u3/src/components/social/EmbedOG.tsx b/apps/u3/src/components/social/EmbedOG.tsx new file mode 100644 index 00000000..2c2cacf2 --- /dev/null +++ b/apps/u3/src/components/social/EmbedOG.tsx @@ -0,0 +1,46 @@ +import { OGData } from '@/services/social/types'; + +export default function EmbedOG({ data, url }: { data: OGData; url: string }) { + const img = data.ogImage[0]?.url; + return ( +
{ + e.stopPropagation(); + window.open(url, '_blank'); + }} + > +
+ +
+
+

+ {data.ogTitle} +

+ {data.ogDescription && ( +

+ {data.ogDescription} +

+ )} + +
+
+ ); +} diff --git a/apps/u3/src/components/social/farcaster/FCast.tsx b/apps/u3/src/components/social/farcaster/FCast.tsx index 131e03cf..587e05a2 100644 --- a/apps/u3/src/components/social/farcaster/FCast.tsx +++ b/apps/u3/src/components/social/farcaster/FCast.tsx @@ -319,6 +319,7 @@ export default function FCast({ {!simpleLayout && ( +> { + return axios({ + url: `${REACT_APP_API_SOCIAL_URL}/3r-farcaster/embedv2`, + method: 'get', + params: { + urls, + timeout: 3000, + maxRedirects: 2, + }, + }); +} + export function getFarcasterEmbedCast({ fid, hash, diff --git a/apps/u3/src/services/social/types/index.ts b/apps/u3/src/services/social/types/index.ts index b79c2dba..36691356 100644 --- a/apps/u3/src/services/social/types/index.ts +++ b/apps/u3/src/services/social/types/index.ts @@ -1,3 +1,5 @@ +import type { Frame } from 'frames.js'; + export enum ApiRespCode { SUCCESS = 0, ERROR = 1, @@ -38,6 +40,19 @@ export type FarCastEmbedMeta = { fcFrameInputText?: string; }; +export type OGData = { + ogDescription: string; + ogImage: { url: string }[]; + ogTitle: string; +}; + +export type FarCastEmbedMetaV2 = { + url: string; + frame: Frame; + ogData: OGData; + metadata?: FarCastEmbedMeta; +}; + export type FarCast = { created_at: string; createdAt: string; diff --git a/apps/u3/src/utils/social/farcaster/getEmbeds.ts b/apps/u3/src/utils/social/farcaster/getEmbeds.ts index 2f7a8bb7..3121cb09 100644 --- a/apps/u3/src/utils/social/farcaster/getEmbeds.ts +++ b/apps/u3/src/utils/social/farcaster/getEmbeds.ts @@ -9,10 +9,23 @@ export function isImg(url?: string) { url.endsWith('.gif') ); } + +export function isVideo(url?: string) { + if (!url) return false; + return ( + url.endsWith('.mp4') || + url.endsWith('.mov') || + url.endsWith('.avi') || + url.endsWith('.webm') || + url.endsWith('.mkv') || + url.endsWith('.m3u8') + ); +} export function getEmbeds(cast: FarCast): { imgs: { url: string; }[]; + videos: { url: string }[]; webpages: { url: string; }[]; @@ -21,8 +34,11 @@ export function getEmbeds(cast: FarCast): { }[]; } { const imgs = []; + const videos = []; const webpages = []; const casts = []; + + console.log('cast', cast.embeds); for (const embed of cast.embeds) { if (embed?.castId) { casts.push(embed); @@ -31,6 +47,10 @@ export function getEmbeds(cast: FarCast): { imgs.push({ url: embed.url, }); + } else if (isVideo(embed.url)) { + videos.push({ + url: embed.url, + }); } else { webpages.push({ url: embed.url, @@ -38,5 +58,5 @@ export function getEmbeds(cast: FarCast): { } } } - return { imgs, webpages, casts }; + return { imgs, webpages, casts, videos }; } From 8db3fd6dc0060765e44ee941dd69c8248a2f1d67 Mon Sep 17 00:00:00 2001 From: ttang Date: Tue, 5 Mar 2024 17:00:23 +0800 Subject: [PATCH 3/7] feat: update hub-web --- apps/u3/package.json | 2 +- apps/u3/src/components/social/EmbedFrame.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/u3/package.json b/apps/u3/package.json index 73afc0b2..5c8f7a5a 100644 --- a/apps/u3/package.json +++ b/apps/u3/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@airstack/airstack-react": "^0.4.7", - "@farcaster/hub-web": "^0.8.2", + "@farcaster/hub-web": "^0.8.4", "@lens-protocol/metadata": "^1.1.5", "@lens-protocol/react-web": "^2.0.0-alpha.33", "@lens-protocol/wagmi": "^4.0.0-alpha.2", diff --git a/apps/u3/src/components/social/EmbedFrame.tsx b/apps/u3/src/components/social/EmbedFrame.tsx index 7eb884ac..9f06fe6d 100644 --- a/apps/u3/src/components/social/EmbedFrame.tsx +++ b/apps/u3/src/components/social/EmbedFrame.tsx @@ -63,6 +63,7 @@ export default function EmbedCastFrame({ castId, inputText: Buffer.from(frameText), state: Buffer.from(''), + transactionId: Buffer.from(''), }, { fid: currFid, @@ -87,6 +88,7 @@ export default function EmbedCastFrame({ hash: toHex(castId.hash), }, state: '', + transactionId: '', }; const trustedData = { messageBytes: Buffer.from( From c54362ee4172324b77841bebb1dc0aec239796ea Mon Sep 17 00:00:00 2001 From: shixuewen Date: Wed, 6 Mar 2024 11:22:28 +0800 Subject: [PATCH 4/7] fix: replyCastAction dependencies --- apps/u3/src/container/social/FarcasterPostDetail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/u3/src/container/social/FarcasterPostDetail.tsx b/apps/u3/src/container/social/FarcasterPostDetail.tsx index eb1e378d..74384449 100644 --- a/apps/u3/src/container/social/FarcasterPostDetail.tsx +++ b/apps/u3/src/container/social/FarcasterPostDetail.tsx @@ -102,7 +102,7 @@ export default function FarcasterPostDetail() { // setIsPending(false); } }, - [cast, loadCastInfo] + [cast, loadCastInfo, isLoginU3, isConnected, encryptedSigner] ); useEffect(() => { From a3ff5156a6c375ec3faa956fa29b886cdc5447fd Mon Sep 17 00:00:00 2001 From: ttang Date: Wed, 6 Mar 2024 16:51:18 +0800 Subject: [PATCH 5/7] feat: ogData img --- apps/u3/src/components/social/EmbedFrame.tsx | 3 ++- apps/u3/src/components/social/EmbedOG.tsx | 5 ++++- apps/u3/src/utils/social/farcaster/getEmbeds.ts | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/u3/src/components/social/EmbedFrame.tsx b/apps/u3/src/components/social/EmbedFrame.tsx index 9f06fe6d..a0199ca9 100644 --- a/apps/u3/src/components/social/EmbedFrame.tsx +++ b/apps/u3/src/components/social/EmbedFrame.tsx @@ -38,7 +38,8 @@ export default function EmbedCastFrame({ const postFrameAction = useCallback( async (index: number, action: string, target?: string) => { if (action === 'link') { - setFrameRedirect(url); + // c637bf4bb03cb409f54afa926689312f1960af85 + setFrameRedirect(target || url); return; } if (action === 'mint') { diff --git a/apps/u3/src/components/social/EmbedOG.tsx b/apps/u3/src/components/social/EmbedOG.tsx index 2c2cacf2..a33631d3 100644 --- a/apps/u3/src/components/social/EmbedOG.tsx +++ b/apps/u3/src/components/social/EmbedOG.tsx @@ -1,7 +1,10 @@ import { OGData } from '@/services/social/types'; export default function EmbedOG({ data, url }: { data: OGData; url: string }) { - const img = data.ogImage[0]?.url; + const img = data.ogImage?.[0]?.url; + if (!img) { + // console.warn('NoImageOG', data, url); + } return (
Date: Wed, 6 Mar 2024 17:02:04 +0800 Subject: [PATCH 6/7] feat: img maybe ud --- apps/u3/src/components/social/EmbedOG.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/u3/src/components/social/EmbedOG.tsx b/apps/u3/src/components/social/EmbedOG.tsx index a33631d3..7432e98d 100644 --- a/apps/u3/src/components/social/EmbedOG.tsx +++ b/apps/u3/src/components/social/EmbedOG.tsx @@ -2,9 +2,7 @@ import { OGData } from '@/services/social/types'; export default function EmbedOG({ data, url }: { data: OGData; url: string }) { const img = data.ogImage?.[0]?.url; - if (!img) { - // console.warn('NoImageOG', data, url); - } + return (
-
- -
+ {img && ( +
+ +
+ )}

{data.ogTitle} From 8931e29ccedeb0ea60f0e0767d94017f0432b853 Mon Sep 17 00:00:00 2001 From: ttang Date: Fri, 8 Mar 2024 17:02:21 +0800 Subject: [PATCH 7/7] feat: frame tx --- apps/u3/src/components/social/EmbedFrame.tsx | 170 ++++++++++++++++++- apps/u3/src/services/social/api/farcaster.ts | 8 + 2 files changed, 172 insertions(+), 6 deletions(-) diff --git a/apps/u3/src/components/social/EmbedFrame.tsx b/apps/u3/src/components/social/EmbedFrame.tsx index a0199ca9..1517f924 100644 --- a/apps/u3/src/components/social/EmbedFrame.tsx +++ b/apps/u3/src/components/social/EmbedFrame.tsx @@ -2,8 +2,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useCallback, useState } from 'react'; import { Frame } from 'frames.js'; +import { + sendTransaction, + simulateContract, + switchChain, + waitForTransactionReceipt, + writeContract, +} from '@wagmi/core'; +import { useAccount, useConfig } from 'wagmi'; + import { CastId, Message, makeFrameAction } from '@farcaster/hub-web'; -import { toHex } from 'viem'; +import { formatEther, parseEther, toHex } from 'viem'; import { toast } from 'react-toastify'; import { Cross2Icon, CaretLeftIcon } from '@radix-ui/react-icons'; import { FarCast } from '../../services/social/types'; @@ -17,6 +26,7 @@ import { cn } from '@/lib/utils'; import { postFrameActionApi, postFrameActionRedirectApi, + postFrameActionTxApi, } from '@/services/social/api/farcaster'; export default function EmbedCastFrame({ @@ -30,11 +40,128 @@ export default function EmbedCastFrame({ }) { const castId: CastId = useFarcasterCastId({ cast }); const { encryptedSigner, isConnected, currFid } = useFarcasterCtx(); - + const { chain, address } = useAccount(); const [frameText, setFrameText] = useState(''); const [frameRedirect, setFrameRedirect] = useState(''); const [frameData, setFrameData] = useState(data); + const config = useConfig(); + + const reportTransaction = useCallback( + async (txId: string, btnIdx: number, postUrl: string, state?: string) => { + if (!castId) { + console.error('no castId'); + toast.error('cast is required'); + return; + } + if (!encryptedSigner || !currFid) { + console.error('no encryptedSigner'); + toast.error('farcaster login is required'); + return; + } + const trustedDataResult = await makeFrameAction( + { + url: Buffer.from(url), + buttonIndex: btnIdx, + castId, + inputText: Buffer.from(frameText), + state: Buffer.from(state || ''), + transactionId: Buffer.from(txId), + }, + { + fid: currFid, + network: FARCASTER_NETWORK, + }, + encryptedSigner + ); + if (trustedDataResult.isErr()) { + throw new Error(trustedDataResult.error.message); + } + + const trustedDataValue = trustedDataResult.value; + const untrustedData = { + fid: currFid, + url, + messageHash: toHex(trustedDataValue.hash), + network: FARCASTER_NETWORK, + buttonIndex: btnIdx, + inputText: frameText, + castId: { + fid: castId.fid, + hash: toHex(castId.hash), + }, + state: state || '', + transactionId: txId, + }; + const trustedData = { + messageBytes: Buffer.from( + Message.encode(trustedDataValue).finish() + ).toString('hex'), + }; + const postData = { + actionUrl: postUrl, + untrustedData, + trustedData, + }; + const resp = await postFrameActionApi(postData); + if (resp.data.code !== 0) { + toast.error(resp.data.msg); + return; + } + const { frame } = resp.data.data; + setFrameData(frame); + }, + [frameData, currFid, encryptedSigner, castId, frameText] + ); + + const sendEthTransactionAction = async ({ + txData, + chainId, + }: { + txData: any; + chainId: string; + }) => { + console.log('txData', txData, chainId); + try { + const parsedChainId = parseInt(chainId, 10); + + // Switch chains if the user is not on the right one + if (chain?.id !== parsedChainId) + await switchChain(config, { chainId: parsedChainId }); + + const hash = await sendTransaction(config, { + ...txData, + // value: txData.value ? BigInt(txData.value) : 0n, + chainId: parsedChainId, + }); + // const { request: transferDegenRequest } = await simulateContract(config, { + // ...txData, + // value: txData.value ? BigInt(txData.value) : 0n, + // chainId: parsedChainId, + // }); + // const hash = await writeContract(config, transferDegenRequest); + // const degenTxReceipt = await waitForTransactionReceipt(config, { + // hash: degenTxHash, + // chainId: base.id, + // }); + + const { status } = await waitForTransactionReceipt(config, { + hash, + chainId: parsedChainId, + }); + console.log('tx status', status); + if (status === 'success') { + return hash; + } + console.error('transaction failed', hash, status); + // toast.error(`mint action failed: ${status}`); + } catch (e: any) { + console.error(e); + // toast.error(e.message.split('\n')[0]); + } + return undefined; + }; + const postFrameAction = useCallback( async (index: number, action: string, target?: string) => { if (action === 'link') { @@ -63,7 +190,7 @@ export default function EmbedCastFrame({ buttonIndex: index, castId, inputText: Buffer.from(frameText), - state: Buffer.from(''), + state: Buffer.from(frameData.state || ''), transactionId: Buffer.from(''), }, { @@ -88,7 +215,7 @@ export default function EmbedCastFrame({ fid: castId.fid, hash: toHex(castId.hash), }, - state: '', + state: frameData.state || '', transactionId: '', }; const trustedData = { @@ -97,11 +224,41 @@ export default function EmbedCastFrame({ ).toString('hex'), }; const postData = { - actionUrl: frameData.postUrl, + actionUrl: target || frameData.postUrl, untrustedData, trustedData, + fromAddress: '', }; + if (action === 'tx') { + console.log('tx', target); + postData.actionUrl = target; + postData.fromAddress = address; + const resp = await postFrameActionTxApi(postData); + if (resp.data.code !== 0) { + toast.error(resp.data.msg); + return; + } + const chainId = resp.data.data.chainId.split(':')[1]; + console.log('tx resp', resp.data.data.params); + const txHash = await sendEthTransactionAction({ + txData: resp.data.data.params, + chainId, + }); + if (!txHash) { + toast.error('transaction failed'); + return; + } + setFrameText(''); + await reportTransaction( + txHash, + index, + frameData.postUrl, + frameData.state + ); + return; + } + if (action === 'post_redirect') { const resp = await postFrameActionRedirectApi(postData); if (resp.data.code !== 0) { @@ -116,10 +273,11 @@ export default function EmbedCastFrame({ return; } const { frame } = resp.data.data; + console.log('frame', frame); setFrameData(frame); } }, - [frameData, currFid, encryptedSigner, castId, frameText] + [frameData, currFid, encryptedSigner, castId, frameText, address] ); return ( <> diff --git a/apps/u3/src/services/social/api/farcaster.ts b/apps/u3/src/services/social/api/farcaster.ts index fbbc2180..415f0bf2 100644 --- a/apps/u3/src/services/social/api/farcaster.ts +++ b/apps/u3/src/services/social/api/farcaster.ts @@ -563,6 +563,14 @@ export function postFrameActionRedirectApi(data: any) { }); } +export function postFrameActionTxApi(data: any) { + return axios({ + url: `${REACT_APP_API_SOCIAL_URL}/3r-farcaster/frame-action-tx/proxy`, + method: 'post', + data, + }); +} + export function notifyTipApi(data: { txHash: string; amount: number;