Skip to content

Commit 8f79c5f

Browse files
feat: render multi protocol support with dynamic signers
1 parent f996bc9 commit 8f79c5f

21 files changed

+956
-654
lines changed

packages/render/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@
7676
"default": "./dist/farcaster/index.cjs"
7777
}
7878
},
79+
"./helpers": {
80+
"import": {
81+
"types": "./dist/helpers.d.ts",
82+
"default": "./dist/helpers.js"
83+
},
84+
"require": {
85+
"types": "./dist/helpers.d.cts",
86+
"default": "./dist/helpers.cjs"
87+
}
88+
},
7989
"./ui": {
8090
"react-native": {
8191
"types": "./dist/ui/index.native.d.cts",

packages/render/src/collapsed-frame-ui.tsx

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import type { ImgHTMLAttributes } from "react";
22
import React, { useState } from "react";
33
import type { Frame } from "frames.js";
44
import type { FrameTheme, FrameState } from "./types";
5+
import {
6+
getFrameParseResultFromStackItemBySpecifications,
7+
isPartialFrameParseResult,
8+
} from "./helpers";
59

610
const defaultTheme: Required<FrameTheme> = {
711
buttonBg: "#fff",
@@ -20,7 +24,7 @@ const getThemeWithDefaults = (theme: FrameTheme): FrameTheme => {
2024
};
2125

2226
export type CollapsedFrameUIProps = {
23-
frameState: FrameState<any, any>;
27+
frameState: FrameState;
2428
theme?: FrameTheme;
2529
FrameImage?: React.FC<ImgHTMLAttributes<HTMLImageElement> & { src: string }>;
2630
allowPartialFrame?: boolean;
@@ -34,44 +38,51 @@ export function CollapsedFrameUI({
3438
allowPartialFrame,
3539
}: CollapsedFrameUIProps): React.JSX.Element | null {
3640
const [isImageLoading, setIsImageLoading] = useState(true);
37-
const currentFrame = frameState.currentFrameStackItem;
38-
const isLoading = currentFrame?.status === "pending" || isImageLoading;
41+
const { currentFrameStackItem, specifications } = frameState;
42+
const isLoading =
43+
currentFrameStackItem?.status === "pending" || isImageLoading;
3944
const resolvedTheme = getThemeWithDefaults(theme ?? {});
4045

4146
if (!frameState.homeframeUrl) {
4247
return null;
4348
}
4449

45-
if (!currentFrame) {
50+
if (!currentFrameStackItem) {
4651
return null;
4752
}
4853

49-
if (
50-
currentFrame.status === "done" &&
51-
currentFrame.frameResult.status === "failure" &&
52-
!(
53-
allowPartialFrame &&
54-
// Need at least image and buttons to render a partial frame
55-
currentFrame.frameResult.frame.image &&
56-
currentFrame.frameResult.frame.buttons
57-
)
58-
) {
59-
return null;
54+
if (currentFrameStackItem.status === "done") {
55+
const currentParseResult = getFrameParseResultFromStackItemBySpecifications(
56+
currentFrameStackItem,
57+
specifications
58+
);
59+
60+
if (
61+
currentParseResult.status === "failure" &&
62+
(!allowPartialFrame || !isPartialFrameParseResult(currentParseResult))
63+
) {
64+
return null;
65+
}
6066
}
6167

6268
let frame: Frame | Partial<Frame> | undefined;
6369

64-
if (currentFrame.status === "done") {
65-
frame = currentFrame.frameResult.frame;
70+
if (currentFrameStackItem.status === "done") {
71+
const currentParseResult = getFrameParseResultFromStackItemBySpecifications(
72+
currentFrameStackItem,
73+
specifications
74+
);
75+
76+
frame = currentParseResult.frame;
6677
} else if (
67-
currentFrame.status === "message" ||
68-
currentFrame.status === "doneRedirect"
78+
currentFrameStackItem.status === "message" ||
79+
currentFrameStackItem.status === "doneRedirect"
6980
) {
70-
frame = currentFrame.request.sourceFrame;
71-
} else if (currentFrame.status === "requestError") {
81+
frame = currentFrameStackItem.request.sourceFrame;
82+
} else if (currentFrameStackItem.status === "requestError") {
7283
frame =
73-
"sourceFrame" in currentFrame.request
74-
? currentFrame.request.sourceFrame
84+
"sourceFrame" in currentFrameStackItem.request
85+
? currentFrameStackItem.request.sourceFrame
7586
: undefined;
7687
}
7788

@@ -118,7 +129,7 @@ export function CollapsedFrameUI({
118129
<div className="flex flex-col flex-1 justify-center min-w-[0]">
119130
<span className="font-semibold block truncate">{frame?.title}</span>
120131
<span className="text-gray-500 text-xs block truncate">
121-
{new URL(currentFrame.url).hostname}
132+
{new URL(currentFrameStackItem.url).hostname}
122133
</span>
123134
</div>
124135
{!!frame && !!frame.buttons ? (

packages/render/src/fallback-frame-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { FarcasterFrameContext } from "./farcaster";
1+
import type { FarcasterFrameContext } from "./farcaster/types";
22

33
export const fallbackFrameContext: FarcasterFrameContext = {
44
castId: {

packages/render/src/farcaster/frames.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,14 @@ import type {
1616
SignFrameActionFunc,
1717
} from "../types";
1818
import type { FarcasterSigner } from "./signers";
19-
20-
export type FarcasterFrameContext = {
21-
/** Connected address of user, only sent with transaction data request */
22-
address?: `0x${string}`;
23-
castId: { hash: `0x${string}`; fid: number };
24-
};
19+
import type { FarcasterFrameContext } from "./types";
2520

2621
/** Creates a frame action for use with `useFrame` and a proxy */
27-
export const signFrameAction: SignFrameActionFunc<FarcasterSigner> = async (
28-
actionContext
29-
) => {
22+
export const signFrameAction: SignFrameActionFunc<
23+
FarcasterSigner,
24+
FrameActionBodyPayload,
25+
FarcasterFrameContext
26+
> = async (actionContext) => {
3027
const {
3128
frameButton,
3229
signer,
@@ -179,13 +176,9 @@ function isFarcasterFrameContext(
179176
/**
180177
* Used to create an unsigned frame action when signer is not defined
181178
*/
182-
export async function unsignedFrameAction<
183-
TSignerStorageType = Record<string, unknown>,
184-
TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload,
185-
TFrameContextType extends FrameContext = FarcasterFrameContext,
186-
>(
187-
actionContext: SignerStateActionContext<TSignerStorageType, TFrameContextType>
188-
): Promise<SignedFrameAction<TFrameActionBodyType>> {
179+
export async function unsignedFrameAction(
180+
actionContext: SignerStateActionContext
181+
): Promise<SignedFrameAction> {
189182
const {
190183
frameButton,
191184
target,
@@ -232,6 +225,6 @@ export async function unsignedFrameAction<
232225
trustedData: {
233226
messageBytes: Buffer.from("").toString("hex"),
234227
},
235-
} as unknown as TFrameActionBodyType,
228+
},
236229
});
237230
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from "./frames";
22
export * from "./signers";
33
export * from "./attribution";
4+
export * from "./types";

packages/render/src/farcaster/signers.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1-
import type { SignerStateInstance } from "..";
1+
import type {
2+
AllowedStorageTypes,
3+
FrameActionBodyPayload,
4+
SignerStateInstance,
5+
} from "../types";
6+
import type { FarcasterFrameContext } from "./types";
27

3-
export type FarcasterSignerState<TSignerType = FarcasterSigner | null> =
4-
SignerStateInstance<TSignerType>;
8+
export type FarcasterSignerState<
9+
TSignerType extends AllowedStorageTypes = FarcasterSigner | null,
10+
> = SignerStateInstance<
11+
TSignerType,
12+
FrameActionBodyPayload,
13+
FarcasterFrameContext
14+
>;
515

616
export type FarcasterSignerPendingApproval = {
717
status: "pending_approval";
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type FarcasterFrameContext = {
2+
/** Connected address of user, only sent with transaction data request */
3+
address?: `0x${string}`;
4+
castId: { hash: `0x${string}`; fid: number };
5+
};

packages/render/src/frame-ui.tsx

Lines changed: 46 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { ImgHTMLAttributes } from "react";
22
import React, { useState } from "react";
33
import type { Frame, FrameButton } from "frames.js";
4-
import type {
5-
FrameTheme,
6-
FrameState,
7-
FrameStackMessage,
8-
FrameStackRequestError,
9-
} from "./types";
4+
import type { FrameTheme, FrameState } from "./types";
5+
import {
6+
getErrorMessageFromFramesStackItem,
7+
getFrameParseResultFromStackItemBySpecifications,
8+
isPartialFrameParseResult,
9+
} from "./helpers";
1010

1111
export const defaultTheme: Required<FrameTheme> = {
1212
buttonBg: "#fff",
@@ -90,22 +90,8 @@ function MessageTooltip({
9090
);
9191
}
9292

93-
function getErrorMessageFromFramesStackItem(
94-
item: FrameStackMessage | FrameStackRequestError
95-
): string {
96-
if (item.status === "message") {
97-
return item.message;
98-
}
99-
100-
if (item.requestError instanceof Error) {
101-
return item.requestError.message;
102-
}
103-
104-
return "An error occurred";
105-
}
106-
10793
export type FrameUIProps = {
108-
frameState: FrameState<any, any>;
94+
frameState: FrameState;
10995
theme?: FrameTheme;
11096
FrameImage?: React.FC<ImgHTMLAttributes<HTMLImageElement> & { src: string }>;
11197
allowPartialFrame?: boolean;
@@ -126,8 +112,9 @@ export function FrameUI({
126112
enableImageDebugging,
127113
}: FrameUIProps): React.JSX.Element | null {
128114
const [isImageLoading, setIsImageLoading] = useState(true);
129-
const currentFrame = frameState.currentFrameStackItem;
130-
const isLoading = currentFrame?.status === "pending" || isImageLoading;
115+
const { currentFrameStackItem, specifications } = frameState;
116+
const isLoading =
117+
currentFrameStackItem?.status === "pending" || isImageLoading;
131118
const resolvedTheme = getThemeWithDefaults(theme ?? {});
132119

133120
if (!frameState.homeframeUrl) {
@@ -136,40 +123,50 @@ export function FrameUI({
136123
);
137124
}
138125

139-
if (!currentFrame) {
126+
if (!currentFrameStackItem) {
140127
return null;
141128
}
142129

143-
if (
144-
currentFrame.status === "done" &&
145-
currentFrame.frameResult.status === "failure" &&
146-
!(
147-
allowPartialFrame &&
148-
// Need at least image and buttons to render a partial frame
149-
currentFrame.frameResult.frame.image &&
150-
currentFrame.frameResult.frame.buttons
151-
)
152-
) {
153-
return <MessageTooltip inline message="Invalid frame" variant="error" />;
130+
// check if frame is partial and if partials are allowed
131+
if (currentFrameStackItem.status === "done") {
132+
const currentFrameParseResult =
133+
getFrameParseResultFromStackItemBySpecifications(
134+
currentFrameStackItem,
135+
specifications
136+
);
137+
138+
// not a proper partial frame or partial frames are disabled
139+
if (
140+
currentFrameParseResult.status === "failure" &&
141+
(!allowPartialFrame ||
142+
!isPartialFrameParseResult(currentFrameParseResult))
143+
) {
144+
return <MessageTooltip inline message="Invalid frame" variant="error" />;
145+
}
154146
}
155147

156148
let frame: Frame | Partial<Frame> | undefined;
157149
let debugImage: string | undefined;
158150

159-
if (currentFrame.status === "done") {
160-
frame = currentFrame.frameResult.frame;
151+
if (currentFrameStackItem.status === "done") {
152+
const parseResult = getFrameParseResultFromStackItemBySpecifications(
153+
currentFrameStackItem,
154+
specifications
155+
);
156+
157+
frame = parseResult.frame;
161158
debugImage = enableImageDebugging
162-
? currentFrame.frameResult.framesDebugInfo?.image
159+
? parseResult.framesDebugInfo?.image
163160
: undefined;
164161
} else if (
165-
currentFrame.status === "message" ||
166-
currentFrame.status === "doneRedirect"
162+
currentFrameStackItem.status === "message" ||
163+
currentFrameStackItem.status === "doneRedirect"
167164
) {
168-
frame = currentFrame.request.sourceFrame;
169-
} else if (currentFrame.status === "requestError") {
165+
frame = currentFrameStackItem.request.sourceFrame;
166+
} else if (currentFrameStackItem.status === "requestError") {
170167
frame =
171-
"sourceFrame" in currentFrame.request
172-
? currentFrame.request.sourceFrame
168+
"sourceFrame" in currentFrameStackItem.request
169+
? currentFrameStackItem.request.sourceFrame
173170
: undefined;
174171
}
175172

@@ -182,11 +179,13 @@ export function FrameUI({
182179
<div className="relative w-full" style={{ height: "100%" }}>
183180
{" "}
184181
{/* Ensure the container fills the height */}
185-
{currentFrame.status === "message" ? (
182+
{currentFrameStackItem.status === "message" ? (
186183
<MessageTooltip
187184
inline={!frame || !("image" in frame) || !frame.image}
188-
message={getErrorMessageFromFramesStackItem(currentFrame)}
189-
variant={currentFrame.type === "error" ? "error" : "message"}
185+
message={getErrorMessageFromFramesStackItem(currentFrameStackItem)}
186+
variant={
187+
currentFrameStackItem.type === "error" ? "error" : "message"
188+
}
190189
/>
191190
) : null}
192191
{!!frame && !!frame.image && (

0 commit comments

Comments
 (0)