diff --git a/.changeset/twenty-pugs-roll.md b/.changeset/twenty-pugs-roll.md new file mode 100644 index 000000000..41bfe4168 --- /dev/null +++ b/.changeset/twenty-pugs-roll.md @@ -0,0 +1,7 @@ +--- +"frames.js": minor +"@frames.js/debugger": minor +"@frames.js/render": minor +--- + +feat: multi protocol support diff --git a/packages/debugger/app/components/cast-composer.tsx b/packages/debugger/app/components/cast-composer.tsx index 8452fa809..15b0d4fbe 100644 --- a/packages/debugger/app/components/cast-composer.tsx +++ b/packages/debugger/app/components/cast-composer.tsx @@ -23,6 +23,7 @@ import { useToast } from "@/components/ui/use-toast"; import { ToastAction } from "@radix-ui/react-toast"; import Link from "next/link"; import type { FarcasterSigner } from "@frames.js/render/identity/farcaster"; +import { getFrameParseResultFromStackItemBySpecifications } from "@frames.js/render/helpers"; type CastComposerProps = { composerAction: Partial; @@ -139,11 +140,15 @@ function createDebugUrl(frameUrl: string, currentUrl: string) { } function isAtLeastPartialFrame(stackItem: FrameStackDone): boolean { + const result = getFrameParseResultFromStackItemBySpecifications(stackItem, [ + "farcaster", + ]); + return ( - stackItem.frameResult.status === "success" || - (!!stackItem.frameResult.frame && - !!stackItem.frameResult.frame.buttons && - stackItem.frameResult.frame.buttons.length > 0) + result.status === "success" || + (!!result.frame && + !!result.frame.buttons && + result.frame.buttons.length > 0) ); } diff --git a/packages/debugger/app/components/frame-debugger.tsx b/packages/debugger/app/components/frame-debugger.tsx index dc0018f66..528f8835b 100644 --- a/packages/debugger/app/components/frame-debugger.tsx +++ b/packages/debugger/app/components/frame-debugger.tsx @@ -22,6 +22,7 @@ import { CollapsedFrameUI, defaultTheme, } from "@frames.js/render"; +import { getFrameParseResultFromStackItemBySpecifications } from "@frames.js/render/helpers"; import { FrameImageNext } from "@frames.js/render/next"; import { Table, TableBody, TableCell, TableRow } from "@/components/table"; import { @@ -59,13 +60,10 @@ import { urlSearchParamsToObject } from "../utils/url-search-params-to-object"; import { FrameUI } from "./frame-ui"; import { useToast } from "@/components/ui/use-toast"; import { ToastAction } from "@/components/ui/toast"; -import type { AnonymousSigner } from "@frames.js/render/identity/anonymous"; -import type { LensSigner } from "@frames.js/render/identity/lens"; -import type { FarcasterSigner } from "@frames.js/render/identity/farcaster"; -import type { XmtpSigner } from "@frames.js/render/identity/xmtp"; type FrameDiagnosticsProps = { stackItem: FramesStackItem; + specification: SupportedParsingSpecification; }; function isPropertyExperimental([key, value]: [string, string]) { @@ -73,7 +71,7 @@ function isPropertyExperimental([key, value]: [string, string]) { return false; } -function FrameDiagnostics({ stackItem }: FrameDiagnosticsProps) { +function FrameDiagnostics({ stackItem, specification }: FrameDiagnosticsProps) { const properties = useMemo(() => { /** tuple of key and value */ const validProperties: [string, string][] = []; @@ -97,7 +95,9 @@ function FrameDiagnostics({ stackItem }: FrameDiagnosticsProps) { return { validProperties, invalidProperties, isValid: true }; } - const result = stackItem.frameResult; + const result = getFrameParseResultFromStackItemBySpecifications(stackItem, [ + specification, + ]); // we need to check validation errors first because getFrame incorrectly return a value for a key even if it's invalid for (const [key, reports] of Object.entries(result.reports)) { @@ -160,8 +160,8 @@ function FrameDiagnostics({ stackItem }: FrameDiagnosticsProps) { {stackItem.speed > 5 ? `Request took more than 5s (${stackItem.speed} seconds). This may be normal: first request will take longer in development (as next.js builds), but in production, clients will timeout requests after 5s` : stackItem.speed > 4 - ? `Warning: Request took more than 4s (${stackItem.speed} seconds). Requests will fail at 5s. This may be normal: first request will take longer in development (as next.js builds), but in production, if there's variance here, requests could fail in production if over 5s` - : `${stackItem.speed} seconds`} + ? `Warning: Request took more than 4s (${stackItem.speed} seconds). Requests will fail at 5s. This may be normal: first request will take longer in development (as next.js builds), but in production, if there's variance here, requests could fail in production if over 5s` + : `${stackItem.speed} seconds`} {properties.validProperties.map(([propertyKey, value]) => { @@ -248,7 +248,8 @@ function ShortenedText({ const FramesRequestCardContentIcon: React.FC<{ stackItem: FramesStackItem; -}> = ({ stackItem }) => { + specification: SupportedParsingSpecification; +}> = ({ stackItem, specification }) => { if (stackItem.status === "pending") { return ; } @@ -269,11 +270,15 @@ const FramesRequestCardContentIcon: React.FC<{ return ; } - if (stackItem.frameResult?.status === "failure") { + const result = getFrameParseResultFromStackItemBySpecifications(stackItem, [ + specification, + ]); + + if (result.status === "failure") { return ; } - if (hasWarnings(stackItem.frameResult.reports)) { + if (hasWarnings(result.reports)) { return ; } @@ -282,10 +287,9 @@ const FramesRequestCardContentIcon: React.FC<{ const FramesRequestCardContent: React.FC<{ stack: FramesStack; - fetchFrame: FrameState< - FarcasterSigner | XmtpSigner | LensSigner | AnonymousSigner | null - >["fetchFrame"]; -}> = ({ fetchFrame, stack }) => { + fetchFrame: FrameState["fetchFrame"]; + specification: SupportedParsingSpecification; +}> = ({ fetchFrame, specification, stack }) => { return stack.map((frameStackItem, i) => { return ( diff --git a/packages/render/src/fallback-frame-context.ts b/packages/render/src/fallback-frame-context.ts index 8c5ceef3d..2c9f5b76a 100644 --- a/packages/render/src/fallback-frame-context.ts +++ b/packages/render/src/fallback-frame-context.ts @@ -1,4 +1,4 @@ -import type { FarcasterFrameContext } from "./farcaster"; +import type { FarcasterFrameContext } from "./farcaster/types"; export const fallbackFrameContext: FarcasterFrameContext = { castId: { diff --git a/packages/render/src/farcaster/frames.tsx b/packages/render/src/farcaster/frames.tsx index 9341fc322..a0925fbce 100644 --- a/packages/render/src/farcaster/frames.tsx +++ b/packages/render/src/farcaster/frames.tsx @@ -16,17 +16,14 @@ import type { SignFrameActionFunc, } from "../types"; import type { FarcasterSigner } from "./signers"; - -export type FarcasterFrameContext = { - /** Connected address of user, only sent with transaction data request */ - address?: `0x${string}`; - castId: { hash: `0x${string}`; fid: number }; -}; +import type { FarcasterFrameContext } from "./types"; /** Creates a frame action for use with `useFrame` and a proxy */ -export const signFrameAction: SignFrameActionFunc = async ( - actionContext -) => { +export const signFrameAction: SignFrameActionFunc< + FarcasterSigner, + FrameActionBodyPayload, + FarcasterFrameContext +> = async (actionContext) => { const { frameButton, signer, @@ -179,13 +176,9 @@ function isFarcasterFrameContext( /** * Used to create an unsigned frame action when signer is not defined */ -export async function unsignedFrameAction< - TSignerStorageType = Record, - TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - TFrameContextType extends FrameContext = FarcasterFrameContext, ->( - actionContext: SignerStateActionContext -): Promise> { +export async function unsignedFrameAction( + actionContext: SignerStateActionContext +): Promise { const { frameButton, target, @@ -232,6 +225,6 @@ export async function unsignedFrameAction< trustedData: { messageBytes: Buffer.from("").toString("hex"), }, - } as unknown as TFrameActionBodyType, + }, }); } diff --git a/packages/render/src/farcaster/index.ts b/packages/render/src/farcaster/index.ts index 6c8b3f2b3..1816ee9fb 100644 --- a/packages/render/src/farcaster/index.ts +++ b/packages/render/src/farcaster/index.ts @@ -1,3 +1,4 @@ export * from "./frames"; export * from "./signers"; export * from "./attribution"; +export * from "./types"; diff --git a/packages/render/src/farcaster/signers.tsx b/packages/render/src/farcaster/signers.tsx index 4c26f5d4d..5740446c5 100644 --- a/packages/render/src/farcaster/signers.tsx +++ b/packages/render/src/farcaster/signers.tsx @@ -1,7 +1,17 @@ -import type { SignerStateInstance } from ".."; +import type { + AllowedStorageTypes, + FrameActionBodyPayload, + SignerStateInstance, +} from "../types"; +import type { FarcasterFrameContext } from "./types"; -export type FarcasterSignerState = - SignerStateInstance; +export type FarcasterSignerState< + TSignerType extends AllowedStorageTypes = FarcasterSigner | null, +> = SignerStateInstance< + TSignerType, + FrameActionBodyPayload, + FarcasterFrameContext +>; export type FarcasterSignerPendingApproval = { status: "pending_approval"; diff --git a/packages/render/src/farcaster/types.ts b/packages/render/src/farcaster/types.ts new file mode 100644 index 000000000..cce4d8425 --- /dev/null +++ b/packages/render/src/farcaster/types.ts @@ -0,0 +1,5 @@ +export type FarcasterFrameContext = { + /** Connected address of user, only sent with transaction data request */ + address?: `0x${string}`; + castId: { hash: `0x${string}`; fid: number }; +}; diff --git a/packages/render/src/frame-ui.tsx b/packages/render/src/frame-ui.tsx index b13e09d44..2f810e4e3 100644 --- a/packages/render/src/frame-ui.tsx +++ b/packages/render/src/frame-ui.tsx @@ -1,12 +1,17 @@ import type { ImgHTMLAttributes } from "react"; import React, { useState } from "react"; -import type { Frame, FrameButton } from "frames.js"; import type { - FrameTheme, - FrameState, - FrameStackMessage, - FrameStackRequestError, -} from "./types"; + Frame, + FrameButton, + SupportedParsingSpecification, +} from "frames.js"; +import type { ParseFramesWithReportsResult } from "frames.js/frame-parsers"; +import type { FrameTheme, FrameState } from "./types"; +import { + getErrorMessageFromFramesStackItem, + getFrameParseResultFromStackItemBySpecifications, + isPartialFrameParseResult, +} from "./helpers"; export const defaultTheme: Required = { buttonBg: "#fff", @@ -90,22 +95,8 @@ function MessageTooltip({ ); } -function getErrorMessageFromFramesStackItem( - item: FrameStackMessage | FrameStackRequestError -): string { - if (item.status === "message") { - return item.message; - } - - if (item.requestError instanceof Error) { - return item.requestError.message; - } - - return "An error occurred"; -} - export type FrameUIProps = { - frameState: FrameState; + frameState: FrameState; theme?: FrameTheme; FrameImage?: React.FC & { src: string }>; allowPartialFrame?: boolean; @@ -126,8 +117,9 @@ export function FrameUI({ enableImageDebugging, }: FrameUIProps): React.JSX.Element | null { const [isImageLoading, setIsImageLoading] = useState(true); - const currentFrame = frameState.currentFrameStackItem; - const isLoading = currentFrame?.status === "pending" || isImageLoading; + const { currentFrameStackItem, specifications } = frameState; + const isLoading = + currentFrameStackItem?.status === "pending" || isImageLoading; const resolvedTheme = getThemeWithDefaults(theme ?? {}); if (!frameState.homeframeUrl) { @@ -136,41 +128,58 @@ export function FrameUI({ ); } - if (!currentFrame) { + if (!currentFrameStackItem) { return null; } - if ( - currentFrame.status === "done" && - currentFrame.frameResult.status === "failure" && - !( - allowPartialFrame && - // Need at least image and buttons to render a partial frame - currentFrame.frameResult.frame.image && - currentFrame.frameResult.frame.buttons - ) - ) { - return ; + // check if frame is partial and if partials are allowed + if (currentFrameStackItem.status === "done") { + const currentFrameParseResult = + getFrameParseResultFromStackItemBySpecifications( + currentFrameStackItem, + specifications + ); + + // not a proper partial frame or partial frames are disabled + if ( + currentFrameParseResult.status === "failure" && + (!allowPartialFrame || + !isPartialFrameParseResult(currentFrameParseResult)) + ) { + return ; + } } let frame: Frame | Partial | undefined; + let parseResult: ParseFramesWithReportsResult | undefined; + let specification: SupportedParsingSpecification | undefined; let debugImage: string | undefined; - if (currentFrame.status === "done") { - frame = currentFrame.frameResult.frame; + if (currentFrameStackItem.status === "done") { + const parseResultBySpec = getFrameParseResultFromStackItemBySpecifications( + currentFrameStackItem, + specifications + ); + + frame = parseResultBySpec.frame; + parseResult = currentFrameStackItem.parseResult; + specification = parseResultBySpec.specification; debugImage = enableImageDebugging - ? currentFrame.frameResult.framesDebugInfo?.image + ? parseResultBySpec.framesDebugInfo?.image : undefined; } else if ( - currentFrame.status === "message" || - currentFrame.status === "doneRedirect" + currentFrameStackItem.status === "message" || + currentFrameStackItem.status === "doneRedirect" ) { - frame = currentFrame.request.sourceFrame; - } else if (currentFrame.status === "requestError") { - frame = - "sourceFrame" in currentFrame.request - ? currentFrame.request.sourceFrame - : undefined; + frame = currentFrameStackItem.request.sourceFrame; + parseResult = currentFrameStackItem.request.sourceParseResult; + specification = currentFrameStackItem.request.specification; + } else if (currentFrameStackItem.status === "requestError") { + if ("sourceFrame" in currentFrameStackItem.request) { + frame = currentFrameStackItem.request.sourceFrame; + parseResult = currentFrameStackItem.request.sourceParseResult; + specification = currentFrameStackItem.request.specification; + } } const ImageEl = FrameImage ? FrameImage : "img"; @@ -182,11 +191,13 @@ export function FrameUI({
{" "} {/* Ensure the container fills the height */} - {currentFrame.status === "message" ? ( + {currentFrameStackItem.status === "message" ? ( ) : null} {!!frame && !!frame.image && ( @@ -234,7 +245,11 @@ export function FrameUI({ }} /> ) : null} - {!!frame && !!frame.buttons && frame.buttons.length > 0 ? ( + {!!parseResult && + !!specification && + !!frame && + !!frame.buttons && + frame.buttons.length > 0 ? (
{frame.buttons.map((frameButton: FrameButton, index: number) => (