diff --git a/apps/u3/package.json b/apps/u3/package.json index dd7bd89c..c09968d8 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.7.3", + "@farcaster/hub-web": "^0.8.5", "@lens-protocol/metadata": "^1.1.5", "@lens-protocol/react-web": "^2.0.0-alpha.33", "@lens-protocol/wagmi": "^4.0.0-alpha.2", @@ -60,6 +60,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", @@ -131,7 +132,7 @@ "yup": "^0.32.11" }, "scripts": { - "start": "craco start", + "start": "cross-env NODE_OPTIONS=--max-old-space-size=8192 craco start", "build": "craco build", "build:report": "craco build --report", "test": "craco test", diff --git a/apps/u3/src/components/social/Embed.tsx b/apps/u3/src/components/social/Embed.tsx index 1b77ba93..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,237 +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(data.url), - buttonIndex: index, - castId, - inputText: Buffer.from(frameText), - }, - { - 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.fcFramePostUrl || frameData.url, - messageHash: toHex(trustedDataValue.hash), - network: FARCASTER_NETWORK, - buttonIndex: index, - inputText: frameText, - castId: { - fid: castId.fid, - hash: toHex(castId.hash), - }, - }; - const trustedData = { - messageBytes: Buffer.from( - Message.encode(trustedDataValue).finish() - ).toString('hex'), - }; - const postData = { - actionUrl: frameData.fcFramePostUrl || frameData.url, - 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(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 || ''); - } - }, - [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..9862b963 --- /dev/null +++ b/apps/u3/src/components/social/EmbedFrame.tsx @@ -0,0 +1,489 @@ +/* eslint-disable react/no-array-index-key */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useCallback, useMemo, useState } from 'react'; +import { Frame } from 'frames.js'; +import { + sendTransaction, + simulateContract, + switchChain, + waitForTransactionReceipt, + writeContract, +} from '@wagmi/core'; +import { useAccount, useConfig, useChains } from 'wagmi'; + +import { CastId, Message, makeFrameAction } from '@farcaster/hub-web'; +import { formatEther, fromHex, parseEther, 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, + postFrameActionTxApi, +} from '@/services/social/api/farcaster'; +import { shortAddress } from '@/utils/message/xmtp'; + +export default function EmbedCastFrame({ + url, + data, + cast, +}: { + url: string; + data: Frame; + cast: FarCast; +}) { + 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 [txSimulate, setTxSimulate] = useState([]); + const [txData, setTxData] = useState(); + const [txBtnIdx, setTxBtnIdx] = useState(0); + + const config = useConfig(); + + const generateFrameActionData = useCallback( + async (btnIdx: number, txId?: string) => { + if (!castId) { + console.error('no castId'); + toast.error('cast is required'); + throw new Error('cast is required'); + } + if (!encryptedSigner || !currFid) { + console.error('no encryptedSigner'); + toast.error('farcaster login is required'); + throw new Error('farcaster login is required'); + } + const trustedDataResult = await makeFrameAction( + { + url: Buffer.from(url), + buttonIndex: btnIdx, + castId, + inputText: Buffer.from(frameText), + state: Buffer.from(frameData.state || ''), + transactionId: Buffer.from(txId || ''), + address: + address && txId ? fromHex(address, 'bytes') : Buffer.from(''), + }, + { + fid: currFid, + network: FARCASTER_NETWORK, + }, + encryptedSigner + ); + if (trustedDataResult.isErr()) { + throw new Error(trustedDataResult.error.message); + } + + const trustedDataValue = trustedDataResult.value; + const untrustedData = { + fid: Number(currFid), + url, + messageHash: toHex(trustedDataValue.hash), + network: FARCASTER_NETWORK, + buttonIndex: trustedDataResult.value.data.frameActionBody.buttonIndex, + timestamp: trustedDataResult.value.data.timestamp, + inputText: frameText, + castId: { + fid: Number(castId.fid), + hash: toHex(castId.hash), + }, + state: frameData.state || '', + transactionId: txId || '', + address: address && txId ? address : '', + }; + const trustedData = { + messageBytes: Buffer.from( + Message.encode(trustedDataValue).finish() + ).toString('hex'), + }; + return { + untrustedData, + trustedData, + }; + }, + [address, castId.fid, castId.hash, currFid, frameData, url] + ); + + const reportTransaction = useCallback( + async (txId: string, btnIdx: number, postUrl: string, state?: string) => { + const { untrustedData, trustedData } = await generateFrameActionData( + btnIdx, + txId + ); + 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, address] + ); + + const sendEthTransactionAction = useCallback(async () => { + // console.log('txData', txData); + if (!txData) { + return undefined; + } + + const chainId = txData.chainId.split(':')[1]; + + 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.params, + // value: txData.value ? BigInt(txData.value) : 0n, + chainId: parsedChainId, + }); + + 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; + }, [txData]); + + const postFrameAction = useCallback( + async (index: number, action: string, target?: string) => { + if (action === 'link') { + // c637bf4bb03cb409f54afa926689312f1960af85 + setFrameRedirect(target || url); + return; + } + if (action === 'mint') { + // website mint + setFrameRedirect(url); + return; + } + const { untrustedData, trustedData } = await generateFrameActionData( + index + ); + const postData = { + 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 { txData: transactionData, simulateResult } = resp.data.data; + setTxData(transactionData); + setTxSimulate(simulateResult); + setTxBtnIdx(index); + return; + } + + 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; + console.log('frame', frame); + setFrameData(frame); + } + }, + [frameData, currFid, encryptedSigner, castId, frameText, address] + ); + return ( + <> +
{ + e.stopPropagation(); + }} + > +
+ +
+ {(frameData.inputText && ( +
+ { + setFrameText(e.target.value); + }} + /> +
+ )) || + null} + {(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} + + ); + })} +
+ )) || + null} +
+ {(frameRedirect && ( + { + setFrameRedirect(''); + }} + /> + )) || + null} + {(txBtnIdx && ( + { + try { + const txId = await sendEthTransactionAction(); + if (txId) { + await reportTransaction(txId, txBtnIdx, frameData.postUrl); + } + } catch (e: any) { + console.error(e); + } + }} + close={() => { + setTxBtnIdx(0); + }} + /> + )) || + null} + + ); +} + +function EmbedCastFrameTxSimulate({ + walletAddress, + txBtnIdx, + txSimulate, + txData, + txAction, + close, +}: { + walletAddress: string; + txBtnIdx: number; + txSimulate: any[]; + txData: any; + txAction: () => void; + close: () => void; +}) { + const chains = useChains(); + const from = useMemo(() => { + return txSimulate.find( + (item) => item.from?.toLowerCase() === walletAddress.toLowerCase() + ); + }, [txSimulate]); + const to = useMemo(() => { + return txSimulate.find( + (item) => item.to?.toLowerCase() === walletAddress.toLowerCase() + ); + }, [txSimulate]); + const chainId = txData.chainId.split(':')[1]; + const chain = chains.find((item) => item.id === parseInt(chainId, 10)); + return ( + +
{ + e.stopPropagation(); + }} + > +
+

⚠️ Transaction

+ +
+
+
+ {from && ( +
+

Send

+

+ {from.amount?.slice(0, 7)} + {from.token_info.symbol} +

+
+ )} + {to && ( +
+

Receive

+

+ {to.amount?.slice(0, 7)} + {to.token_info.symbol} +

+
+ )} +
+ +
+

+ chain: {chain.name} +

+

+ wallet address:{' '} + + {shortAddress(walletAddress)} + +

+
+
+
+ + +
+
+
+ ); +} + +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..7432e98d --- /dev/null +++ b/apps/u3/src/components/social/EmbedOG.tsx @@ -0,0 +1,49 @@ +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'); + }} + > + {img && ( +
+ +
+ )} +
+

+ {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 08d447c0..b62312cd 100644 --- a/apps/u3/src/components/social/farcaster/FCast.tsx +++ b/apps/u3/src/components/social/farcaster/FCast.tsx @@ -327,6 +327,7 @@ export default function FCast({ {!simpleLayout && ( item?.platform && item?.data) + .splice(0, 6), farcasterUserData: temp, isLoading: false, }); diff --git a/apps/u3/src/container/social/FarcasterPostDetail.tsx b/apps/u3/src/container/social/FarcasterPostDetail.tsx index 349cd111..19340c50 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(() => { diff --git a/apps/u3/src/hooks/social/farcaster/useFarcasterQR.ts b/apps/u3/src/hooks/social/farcaster/useFarcasterQR.ts index d8535cd4..6c9fd422 100644 --- a/apps/u3/src/hooks/social/farcaster/useFarcasterQR.ts +++ b/apps/u3/src/hooks/social/farcaster/useFarcasterQR.ts @@ -39,6 +39,7 @@ import { setDefaultFarcaster, } from '@/utils/social/farcaster/farcaster-default'; import useLogin from '@/hooks/shared/useLogin'; +import { REACT_APP_API_SOCIAL_URL } from '@/constants'; const stopSign = { stop: false, @@ -104,7 +105,7 @@ export default function useFarcasterQR() { let signerSuccess = false; let stopped = false; - while (tries < 60) { + while (tries < 100) { if (stopSign.stop) { stopped = true; break; @@ -114,12 +115,12 @@ export default function useFarcasterQR() { await new Promise((resolve) => setTimeout(resolve, 2000)); const result = await axios - .get(`${WARPCAST_API}/v2/signed-key-request`, { + .get(`${REACT_APP_API_SOCIAL_URL}/3r-farcaster/signed-key-request`, { params: { token, }, }) - .then((response) => response.data.result); + .then((response) => response.data.data.result); const { signedKeyRequest } = result; @@ -185,14 +186,17 @@ export default function useFarcasterQR() { const { signature, appFid, deadline } = resp.data.data; - const { token, deeplinkUrl } = await axios - .post(`${WARPCAST_API}/v2/signed-key-requests`, { + const req = await axios.post( + `${REACT_APP_API_SOCIAL_URL}/3r-farcaster/signer-request`, + { key: convertedKey, requestFid: appFid, signature, deadline, - }) - .then((response) => response.data.result.signedKeyRequest); + } + ); + // console.log('signer-request data', req.data.data); + const { token, deeplinkUrl } = req.data.data; // save-temp saveTempFarsignPrivateKey(keyPair.privateKey); diff --git a/apps/u3/src/services/social/api/farcaster.ts b/apps/u3/src/services/social/api/farcaster.ts index 06cd125e..e7f5ca14 100644 --- a/apps/u3/src/services/social/api/farcaster.ts +++ b/apps/u3/src/services/social/api/farcaster.ts @@ -1,6 +1,12 @@ import axios, { AxiosPromise } from 'axios'; import { UrlMetadata } from '@mod-protocol/core'; -import { ApiResp, FarCast, FarCastEmbedMeta, SocialPlatform } from '../types'; +import { + ApiResp, + FarCast, + FarCastEmbedMeta, + FarCastEmbedMetaV2, + SocialPlatform, +} from '../types'; import { REACT_APP_API_SOCIAL_URL } from '../../../constants'; import request from '@/services/shared/api/request'; import { FollowType } from '@/container/profile/Contacts'; @@ -106,6 +112,22 @@ export function getFarcasterEmbedMetadata(urls: string[]): AxiosPromise< }); } +export function getFarcasterEmbedMetadataV2(urls: string[]): AxiosPromise< + ApiResp<{ + metadata: (null | FarCastEmbedMetaV2)[]; + }> +> { + return axios({ + url: `${REACT_APP_API_SOCIAL_URL}/3r-farcaster/embedv2`, + method: 'get', + params: { + urls, + timeout: 3000, + maxRedirects: 2, + }, + }); +} + export function getFarcasterEmbedCast({ fid, hash, @@ -561,6 +583,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; diff --git a/apps/u3/src/services/social/types/index.ts b/apps/u3/src/services/social/types/index.ts index d48ce46e..82b6e089 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..85cd96a0 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,10 @@ export function getEmbeds(cast: FarCast): { }[]; } { const imgs = []; + const videos = []; const webpages = []; const casts = []; + for (const embed of cast.embeds) { if (embed?.castId) { casts.push(embed); @@ -31,6 +46,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 +57,5 @@ export function getEmbeds(cast: FarCast): { } } } - return { imgs, webpages, casts }; + return { imgs, webpages, casts, videos }; }