Skip to content

Commit

Permalink
feat: error support in render, debugger, example
Browse files Browse the repository at this point in the history
  • Loading branch information
stephancill committed Apr 4, 2024
1 parent 54abae4 commit 7c74f35
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 48 deletions.
6 changes: 6 additions & 0 deletions .changeset/real-socks-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@frames.js/debugger": patch
"@frames.js/render": patch
---

feat: frame error message support
5 changes: 5 additions & 0 deletions .changeset/thirty-islands-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"framesjs-starter": patch
---

feat: error example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createFrames } from "frames.js/next";

export const frames = createFrames({
basePath: "/examples/new-api-error/frames",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* eslint-disable react/jsx-key */
import { Button } from "frames.js/next";
import { frames } from "./frames";
import { error } from "frames.js/core";

const handler = frames(async (ctx) => {
if (ctx.message) {
if (!ctx.message.inputText) {
console.log("error");
error("Invalid input: Empty text");
}
}

return {
image: ctx.message?.inputText ? (
<div tw="flex">Entered text: {ctx.message.inputText}</div>
) : (
<div tw="flex flex-col">
<div tw="flex">Enter text</div>
<div tw="flex">Empty text input will throw an error</div>
</div>
),
textInput: "Enter text or leave empty",
buttons: [<Button action="post">Enter</Button>],
};
});

export const GET = handler;
export const POST = handler;
33 changes: 33 additions & 0 deletions examples/framesjs-starter/app/examples/new-api-error/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Link from "next/link";
import { currentURL, vercelURL } from "../../utils";
import { createDebugUrl } from "../../debug";
import type { Metadata } from "next";
import { fetchMetadata } from "frames.js/next";

export async function generateMetadata(): Promise<Metadata> {
return {
title: "New api example",
description: "This is a new api example",
other: {
...(await fetchMetadata(
new URL(
"/examples/new-api-error/frames",
vercelURL() || "http://localhost:3000"
)
)),
},
};
}

export default async function Home() {
const url = currentURL("/examples/new-api-error");

return (
<div>
New api error example.{" "}
<Link href={createDebugUrl(url)} className="underline">
Debug
</Link>
</div>
);
}
5 changes: 5 additions & 0 deletions examples/framesjs-starter/app/examples/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ export default function ExamplesIndexPage() {
Slow requests
</Link>
</li>
<li>
<Link className="underline" href="/examples/new-api-error">
Errors
</Link>
</li>
</ul>
<b>Frames.js v0.8 and below</b>
<ul>
Expand Down
28 changes: 14 additions & 14 deletions packages/debugger/app/components/frame-debugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,7 @@ export function FrameDebugger({
useEffect(() => {
if (!frameState.isLoading) {
// make sure the first frame is open
if (
!openAccordions.includes(
String(latestFrame?.timestamp.getTime())
)
)
if (!openAccordions.includes(String(latestFrame?.timestamp.getTime())))
setOpenAccordions((v) => [
...v,
String(latestFrame?.timestamp.getTime()),
Expand Down Expand Up @@ -275,16 +271,20 @@ export function FrameDebugger({
return (
<button
className={`px-4 py-3 flex flex-col gap-2 ${i !== 0 ? "border-t" : "bg-slate-50"} hover:bg-slate-50 w-full`}
key={frameStackItem.timestamp.getTime()}
key={i}
onClick={() => {
frameState.fetchFrame(frameStackItem.method === 'GET' ? {
method: 'GET',
url: frameStackItem.url,
} : {
url: frameStackItem.url,
method: frameStackItem.method,
request: frameStackItem.request,
});
frameState.fetchFrame(
frameStackItem.method === "GET"
? {
method: "GET",
url: frameStackItem.url,
}
: {
url: frameStackItem.url,
method: frameStackItem.method,
request: frameStackItem.request,
}
);
}}
>
<span className="flex text-left flex-row w-full">
Expand Down
1 change: 1 addition & 0 deletions packages/frames.js/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { concurrentMiddleware } from "../middleware/concurrentMiddleware";
export { composeMiddleware } from "./composeMiddleware";
export { Button } from "./components";
export { redirect } from "./redirect";
export { error } from "./error";
export * as types from "./types";
export * from "./constants";
1 change: 1 addition & 0 deletions packages/render/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class PresentableError extends Error {}
69 changes: 44 additions & 25 deletions packages/render/src/frame-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ImgHTMLAttributes } from "react";
import React, { useEffect, useState } from "react";
import type { FrameButton } from "frames.js";
import type { FrameTheme, FrameState } from "./types";
import { PresentableError } from "./errors";

export const defaultTheme: Required<FrameTheme> = {
buttonBg: "#fff",
Expand Down Expand Up @@ -47,7 +48,7 @@ export function FrameUI({
return <div>Missing frame url</div>;
}

if (frameState.error) {
if (frameState.error && !(frameState.error instanceof PresentableError)) {
return <div>Failed to load Frame</div>;
}

Expand All @@ -69,29 +70,45 @@ export function FrameUI({
style={{ backgroundColor: resolvedTheme.bg }}
className="flex flex-col w-full gap-2 rounded-br rounded-bl"
>
<ImageEl
src={frameState.frame.image}
alt="Frame image"
width="100%"
style={{
filter: isLoading ? "blur(4px)" : undefined,
borderTopLeftRadius: `${resolvedTheme.buttonRadius}px`,
borderTopRightRadius: `${resolvedTheme.buttonRadius}px`,
border: `1px solid ${resolvedTheme.buttonBorderColor}`,
objectFit: "cover",
width: "100%",
aspectRatio:
(frameState.frame.imageAspectRatio ?? "1.91:1") === "1:1"
? "1/1"
: "1.91/1",
}}
onLoad={() => {
setIsImageLoading(false);
}}
onError={() => {
setIsImageLoading(false);
}}
/>
<div className="relative w-full" style={{ height: "100%" }}>
{" "}
{/* Ensure the container fills the height */}
{frameState.error ? (
<div
className="absolute px-4 py-2 rounded-sm"
style={{
zIndex: 2,
backgroundColor: "rgba(0, 0, 0, 0.7)",
color: "white",
}}
>
{(frameState.error as PresentableError).message}
</div>
) : null}
<ImageEl
src={frameState.frame.image}
alt="Frame image"
width="100%"
style={{
filter: isLoading ? "blur(4px)" : undefined,
borderTopLeftRadius: `${resolvedTheme.buttonRadius}px`,
borderTopRightRadius: `${resolvedTheme.buttonRadius}px`,
border: `1px solid ${resolvedTheme.buttonBorderColor}`,
objectFit: "cover",
width: "100%",
aspectRatio:
(frameState.frame.imageAspectRatio ?? "1.91:1") === "1:1"
? "1/1"
: "1.91/1",
}}
onLoad={() => {
setIsImageLoading(false);
}}
onError={() => {
setIsImageLoading(false);
}}
/>
</div>
{frameState.frame.inputText ? (
<input
className="p-[6px] mx-2 border box-border"
Expand Down Expand Up @@ -132,7 +149,9 @@ export function FrameUI({
cursor: isLoading ? undefined : "pointer",
}}
onClick={() => {
Promise.resolve(frameState.onButtonPress(frameButton, index)).catch((e: unknown) => {
Promise.resolve(
frameState.onButtonPress(frameButton, index)
).catch((e: unknown) => {
// eslint-disable-next-line no-console -- provide feedback to the user
console.error(e);
});
Expand Down
13 changes: 10 additions & 3 deletions packages/render/src/next/POST.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { FrameActionPayload} from "frames.js";
import type { FrameActionPayload } from "frames.js";
import { getFrame } from "frames.js";
import type { NextRequest } from "next/server";

/** Proxies frame actions to avoid CORS issues and preserve user IP privacy */
export async function POST(req: NextRequest): Promise<Response> {
const body = await req.json() as FrameActionPayload;
const body = (await req.json()) as FrameActionPayload;
const isPostRedirect =
req.nextUrl.searchParams.get("postType") === "post_redirect";
const isTransactionRequest =
Expand Down Expand Up @@ -35,8 +35,15 @@ export async function POST(req: NextRequest): Promise<Response> {
);
}

if (r.status >= 400 && r.status < 500) {
const json = (await r.json()) as { message?: string };
if ("message" in json) {
return Response.json({ message: json.message }, { status: r.status });
}
}

if (isTransactionRequest) {
const transaction = await r.json() as JSON;
const transaction = (await r.json()) as JSON;
return Response.json(transaction);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/render/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export type FrameStackSuccess = FrameStackBase & {
isValid: boolean;
};

export type FrameStackError = FrameStackBase & {
export type FrameStackError = (FrameStackBase | FrameStackSuccess) & {
requestError: unknown;
};

Expand Down
29 changes: 24 additions & 5 deletions packages/render/src/use-frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
UseFrameReturn,
OnTransactionArgs,
} from "./types";
import { PresentableError } from "./errors";

function onMintFallback({ target }: OnMintArgs): void {
window.alert(`Mint requested: ${target}`);
Expand Down Expand Up @@ -161,7 +162,7 @@ export function useFrame<
if (!response.ok) {
throw new Error(`Failed to fetch frame: ${response.statusText}`);
}

newFrame = (await response.json()) as ReturnType<typeof getFrame>;
const tend = new Date();

Expand Down Expand Up @@ -220,7 +221,16 @@ export function useFrame<
});

if (!response.ok) {
throw new Error(`Failed to fetch frame: ${response.statusText}`);
if (response.status >= 400 && response.status < 500) {
const data = (await response.clone().json()) as {
message?: string;
};
// Show error message if available
throw new PresentableError(data.message);
}

if (response.status >= 500)
throw new Error(`Failed to fetch frame: ${response.statusText}`);
}

const dataRes = (await response.json()) as
Expand Down Expand Up @@ -248,8 +258,12 @@ export function useFrame<
}
} catch (err) {
const tend = new Date();
const baseItem =
err instanceof PresentableError
? { ...framesStack[0], ...frameStackBase }
: frameStackBase;
stackItem = {
...frameStackBase,
...baseItem,
responseStatus: response?.status ?? 500,
requestError: err,
speed: Number(
Expand Down Expand Up @@ -479,7 +493,11 @@ export function useFrame<
}

async function onTransactionRequest({
buttonIndex, postInputText, frameButton, target, state,
buttonIndex,
postInputText,
frameButton,
target,
state,
}: {
frameButton: FrameButton;
buttonIndex: number;
Expand Down Expand Up @@ -527,7 +545,8 @@ export function useFrame<
...body,
}),
});
const transactionResponse = (await response.json()) as TransactionTargetResponse;
const transactionResponse =
(await response.json()) as TransactionTargetResponse;
return transactionResponse;
} catch {
throw new Error(
Expand Down

0 comments on commit 7c74f35

Please sign in to comment.