Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: customizable frame state hook #521

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-rivers-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@frames.js/render": patch
---

feat: customizable frame state for useFrame hook
6 changes: 6 additions & 0 deletions .changeset/grumpy-berries-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@frames.js/debugger": patch
"@frames.js/render": patch
---

feat: useComposerAction hook
98 changes: 6 additions & 92 deletions packages/debugger/app/components/action-debugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { cn } from "@/lib/utils";
import {
type FarcasterFrameContext,
type FrameActionBodyPayload,
OnComposeFormActionFuncReturnType,
defaultTheme,
} from "@frames.js/render";
import { ParsingReport } from "frames.js";
Expand All @@ -26,7 +25,6 @@ import React, {
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react";
import { Button } from "../../@/components/ui/button";
Expand All @@ -37,12 +35,9 @@ import { useFrame } from "@frames.js/render/use-frame";
import { WithTooltip } from "./with-tooltip";
import { useToast } from "@/components/ui/use-toast";
import type { CastActionDefinitionResponse } from "../frames/route";
import { ComposerFormActionDialog } from "./composer-form-action-dialog";
import { AwaitableController } from "../lib/awaitable-controller";
import type { ComposerActionFormResponse } from "frames.js/types";
import { CastComposer, CastComposerRef } from "./cast-composer";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import type { FarcasterSigner } from "@frames.js/render/identity/farcaster";
import { ComposerActionDebugger } from "./composer-action-debugger";

type FrameDebuggerFramePropertiesTableRowsProps = {
actionMetadataItem: CastActionDefinitionResponse;
Expand Down Expand Up @@ -227,47 +222,9 @@ export const ActionDebugger = React.forwardRef<
}
}, [copySuccess, setCopySuccess]);

const [composeFormActionDialogSignal, setComposerFormActionDialogSignal] =
useState<AwaitableController<
OnComposeFormActionFuncReturnType,
ComposerActionFormResponse
> | null>(null);
const actionFrameState = useFrame({
...farcasterFrameConfig,
async onComposerFormAction({ form }) {
try {
const dialogSignal = new AwaitableController<
OnComposeFormActionFuncReturnType,
ComposerActionFormResponse
>(form);

setComposerFormActionDialogSignal(dialogSignal);

const result = await dialogSignal;

// if result is undefined then user closed the dialog window without submitting
// otherwise we have updated data
if (result?.composerActionState) {
castComposerRef.current?.updateState(result.composerActionState);
}

return result;
} catch (e) {
console.error(e);
toast({
title: "Error occurred",
description:
e instanceof Error
? e.message
: "Unexpected error, check the console for more info",
variant: "destructive",
});
} finally {
setComposerFormActionDialogSignal(null);
}
},
});
const castComposerRef = useRef<CastComposerRef>(null);
const [castActionDefinition, setCastActionDefinition] = useState<Exclude<
CastActionDefinitionResponse,
{ status: "failure" }
Expand Down Expand Up @@ -401,57 +358,14 @@ export const ActionDebugger = React.forwardRef<
actionMetadataItem={actionMetadataItem}
onRefreshUrl={() => refreshUrl()}
>
<CastComposer
farcasterFrameConfig={farcasterFrameConfig}
ref={castComposerRef}
composerAction={actionMetadataItem.action}
onComposerActionClick={(composerActionState) => {
if (actionMetadataItem.status !== "success") {
console.error(actionMetadataItem);

toast({
title: "Invalid action metadata",
description:
"Please check the console for more information",
variant: "destructive",
});
return;
}

Promise.resolve(
actionFrameState.onComposerActionButtonPress({
castAction: {
...actionMetadataItem.action,
url: actionMetadataItem.url,
},
composerActionState,
// clear stack, this removes first item that will appear in the debugger
clearStack: true,
})
).catch((e: unknown) => {
// eslint-disable-next-line no-console -- provide feedback to the user
console.error(e);
});
<ComposerActionDebugger
actionMetadata={actionMetadataItem.action}
url={actionMetadataItem.url}
onToggleToCastActionDebugger={() => {
setActiveTab("cast-action");
}}
/>
</ActionInfo>

{!!composeFormActionDialogSignal && (
<ComposerFormActionDialog
connectedAddress={farcasterFrameConfig.connectedAddress}
composerActionForm={composeFormActionDialogSignal.data}
onClose={() => {
composeFormActionDialogSignal.resolve(undefined);
}}
onSave={({ composerState }) => {
composeFormActionDialogSignal.resolve({
composerActionState: composerState,
});
}}
onTransaction={farcasterFrameConfig.onTransaction}
onSignature={farcasterFrameConfig.onSignature}
/>
)}
</TabsContent>
</Tabs>
</>
Expand Down
50 changes: 20 additions & 30 deletions packages/debugger/app/components/cast-composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,21 @@ import {
ExternalLinkIcon,
} from "lucide-react";
import IconByName from "./octicons";
import { useFrame } from "@frames.js/render/use-frame";
import { useFrame_unstable } from "@frames.js/render/use-frame";
import { WithTooltip } from "./with-tooltip";
import type {
FarcasterFrameContext,
FrameActionBodyPayload,
FrameStackDone,
} from "@frames.js/render";
import { fallbackFrameContext } from "@frames.js/render";
import { FrameUI } from "./frame-ui";
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 { useFarcasterIdentity } from "../hooks/useFarcasterIdentity";
import { useAccount } from "wagmi";
import { FrameStackDone } from "@frames.js/render/unstable-types";
import { useDebuggerFrameState } from "../hooks/useDebuggerFrameState";

type CastComposerProps = {
composerAction: Partial<ComposerActionResponse>;
onComposerActionClick: (state: ComposerActionState) => any;
farcasterFrameConfig: Parameters<
typeof useFrame<
FarcasterSigner | null,
FrameActionBodyPayload,
FarcasterFrameContext
>
>[0];
};

export type CastComposerRef = {
Expand All @@ -43,7 +35,7 @@ export type CastComposerRef = {
export const CastComposer = React.forwardRef<
CastComposerRef,
CastComposerProps
>(({ composerAction, farcasterFrameConfig, onComposerActionClick }, ref) => {
>(({ composerAction, onComposerActionClick }, ref) => {
const [state, setState] = useState<ComposerActionState>({
text: "",
embeds: [],
Expand Down Expand Up @@ -79,7 +71,6 @@ export const CastComposer = React.forwardRef<
{state.embeds.slice(0, 2).map((embed, index) => (
<li key={`${embed}-${index}`}>
<CastEmbedPreview
farcasterFrameConfig={farcasterFrameConfig}
onRemove={() => {
const filteredEmbeds = state.embeds.filter(
(_, i) => i !== index
Expand Down Expand Up @@ -119,13 +110,6 @@ export const CastComposer = React.forwardRef<
CastComposer.displayName = "CastComposer";

type CastEmbedPreviewProps = {
farcasterFrameConfig: Parameters<
typeof useFrame<
FarcasterSigner | null,
FrameActionBodyPayload,
FarcasterFrameContext
>
>[0];
url: string;
onRemove: () => void;
};
Expand All @@ -147,15 +131,21 @@ function isAtLeastPartialFrame(stackItem: FrameStackDone): boolean {
);
}

function CastEmbedPreview({
farcasterFrameConfig,
onRemove,
url,
}: CastEmbedPreviewProps) {
function CastEmbedPreview({ onRemove, url }: CastEmbedPreviewProps) {
const account = useAccount();
const { toast } = useToast();
const frame = useFrame({
...farcasterFrameConfig,
const farcasterIdentity = useFarcasterIdentity();
const frame = useFrame_unstable({
frameStateHook: useDebuggerFrameState,
async resolveAddress() {
return account.address ?? null;
},
homeframeUrl: url,
frameActionProxy: "/frames",
frameGetProxy: "/frames",
resolveSigner() {
return farcasterIdentity.withContext(fallbackFrameContext);
},
});

const handleFrameError = useCallback(
Expand Down
51 changes: 51 additions & 0 deletions packages/debugger/app/components/composer-action-debugger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {
ComposerActionResponse,
ComposerActionState,
} from "frames.js/types";
import { CastComposer, CastComposerRef } from "./cast-composer";
import { useRef, useState } from "react";
import { ComposerFormActionDialog } from "./composer-form-action-dialog";
import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity";

type ComposerActionDebuggerProps = {
url: string;
actionMetadata: Partial<ComposerActionResponse>;
onToggleToCastActionDebugger: () => void;
};

export function ComposerActionDebugger({
actionMetadata,
url,
onToggleToCastActionDebugger,
}: ComposerActionDebuggerProps) {
const castComposerRef = useRef<CastComposerRef>(null);
const signer = useFarcasterIdentity();
const [actionState, setActionState] = useState<ComposerActionState | null>(
null
);

return (
<>
<CastComposer
composerAction={actionMetadata}
onComposerActionClick={setActionState}
ref={castComposerRef}
/>
{!!actionState && (
<ComposerFormActionDialog
actionState={actionState}
signer={signer}
url={url}
onClose={() => {
setActionState(null);
}}
onSubmit={(newActionState) => {
castComposerRef.current?.updateState(newActionState);
setActionState(null);
}}
onToggleToCastActionDebugger={onToggleToCastActionDebugger}
/>
)}
</>
);
}
Loading
Loading