Skip to content

Commit

Permalink
feat: Frame now uses FrameMetadata instead of string record (#232)
Browse files Browse the repository at this point in the history
  • Loading branch information
cnasc authored Mar 7, 2024
1 parent 6ef37ea commit 1b17de1
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 53 deletions.
72 changes: 30 additions & 42 deletions framegear/components/Frame/Frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useAtom } from 'jotai';
import { ChangeEvent, PropsWithChildren, useCallback, useMemo, useState } from 'react';
import { ExternalLinkIcon, ResetIcon, RocketIcon } from '@radix-ui/react-icons';
import { useRedirectModal } from '@/components/RedirectModalContext/RedirectModalContext';
import { FrameMetadataWithImageObject } from '@/utils/frameResultToFrameMetadata';

export function Frame() {
const [results] = useAtom(frameResultsAtom);
Expand All @@ -14,40 +15,14 @@ export function Frame() {

const latestFrame = results[results.length - 1];

if (!latestFrame.isValid) {
return <ErrorFrame />;
}

return <ValidFrame tags={latestFrame.tags} />;
return <ValidFrame metadata={latestFrame.metadata} />;
}

function ValidFrame({ tags }: { tags: Record<string, string> }) {
function ValidFrame({ metadata }: { metadata: FrameMetadataWithImageObject }) {
const [inputText, setInputText] = useState('');
const { image, imageAspectRatioClassname, input, buttons } = useMemo(() => {
const image = tags['fc:frame:image'];
const imageAspectRatioClassname =
tags['fc:frame:image:aspect_ratio'] === '1:1' ? 'aspect-square' : 'aspect-[1.91/1]';
const input = tags['fc:frame:input:text'];
// TODO: when debugger is live we will also need to extract actions, etc.
const buttons = [1, 2, 3, 4].map((index) => {
const key = `fc:frame:button:${index}`;
const actionKey = `${key}:action`;
const targetKey = `${key}:target`;
const value = tags[key];
const action = tags[actionKey] || 'post';
const target = tags[targetKey] || tags['fc:frame:post_url'];

// If value exists, we can return the whole object (incl. default values).
// If it doesn't, then the truth is there is no button.
return value ? { key, value, action, target, index } : undefined;
});
return {
image,
imageAspectRatioClassname,
input,
buttons,
};
}, [tags]);
const { image, input, buttons } = metadata;
const imageAspectRatioClassname =
metadata.image.aspectRatio === '1:1' ? 'aspect-square' : 'aspect-[1.91/1]';

const handleInputChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setInputText(e.target.value),
Expand All @@ -59,23 +34,29 @@ function ValidFrame({ tags }: { tags: Record<string, string> }) {
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className={`w-full rounded-t-xl ${imageAspectRatioClassname} object-cover`}
src={image}
src={image.src}
alt=""
/>
<div className="bg-button-gutter-light dark:bg-content-light flex flex-col gap-2 rounded-b-xl px-4 py-2">
{!!input && (
<input
className="bg-input-light border-light rounded-lg border p-2 text-black"
type="text"
placeholder={input}
placeholder={input.text}
onChange={handleInputChange}
/>
)}
<div className="flex flex-wrap gap-4">
{buttons.map((button) =>
{buttons?.map((button, index) =>
button ? (
<FrameButton inputText={inputText} key={button.key} button={button}>
{button.value}
<FrameButton
inputText={inputText}
key={button.label}
index={index + 1}
button={button}
state={metadata.state}
>
{button.label}
</FrameButton>
) : null,
)}
Expand All @@ -89,6 +70,7 @@ function ErrorFrame() {
// TODO: implement -- decide how to handle
// - simply show an error?
// - best effort rendering of what they do have?
// - maybe just ValidFrame with a red border?
return <PlaceholderFrame />;
}

Expand All @@ -97,7 +79,9 @@ function PlaceholderFrame() {
<div className="flex flex-col">
<div className="bg-farcaster flex aspect-[1.91/1] w-full rounded-t-xl"></div>
<div className="bg-button-gutter-light dark:bg-content-light flex flex-wrap gap-2 rounded-b-xl px-4 py-2">
<FrameButton inputText="">Get Started</FrameButton>
<FrameButton state={{}} index={1} inputText="">
Get Started
</FrameButton>
</div>
</div>
);
Expand All @@ -106,11 +90,14 @@ function PlaceholderFrame() {
function FrameButton({
children,
button,
index,
inputText,
state,
}: PropsWithChildren<{
// TODO: this type should probably be extracted
button?: { key: string; value: string; action: string; target: string; index: number };
button?: NonNullable<FrameMetadataWithImageObject['buttons']>[0];
index: number;
inputText: string;
state: any;
}>) {
const { openModal } = useRedirectModal();
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -121,8 +108,9 @@ function FrameButton({
// TODO: collect user options (follow, like, etc.) and include
const confirmAction = async () => {
const result = await postFrame({
buttonIndex: button.index,
url: button.target,
buttonIndex: index,
url: button.target!,
state: JSON.stringify(state),
// TODO: make these user-input-driven
castId: {
fid: 0,
Expand Down Expand Up @@ -152,7 +140,7 @@ function FrameButton({
openModal(onConfirm);
}
// TODO: implement other actions (mint, etc.)
}, [button?.action, button?.index, button?.target, inputText, openModal, setResults]);
}, [button, index, inputText, openModal, setResults, state]);

const buttonIcon = useMemo(() => {
switch (button?.action) {
Expand Down
4 changes: 2 additions & 2 deletions framegear/utils/frameResultToFrameMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('frameResultToFrameMetadata', () => {
undefined,
undefined,
],
image: 'Image URL',
image: { src: 'Image URL', aspectRatio: undefined },
input: { text: 'Input Text' },
postUrl: 'Post URL',
state: { key: 'value' },
Expand All @@ -47,7 +47,7 @@ describe('frameResultToFrameMetadata', () => {

expect(metadata).toEqual({
buttons: [undefined, undefined, undefined, undefined],
image: undefined,
image: { src: undefined, aspectRatio: undefined },
input: undefined,
postUrl: undefined,
state: undefined,
Expand Down
30 changes: 22 additions & 8 deletions framegear/utils/frameResultToFrameMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
import { FrameMetadataType } from '@coinbase/onchainkit';
import { FrameImageMetadata, FrameMetadataType } from '@coinbase/onchainkit';

export function frameResultToFrameMetadata(result: Record<string, string>): FrameMetadataType {
export type FrameMetadataWithImageObject = FrameMetadataType & {
image: FrameImageMetadata;
};

export function frameResultToFrameMetadata(
result: Record<string, string>,
): FrameMetadataWithImageObject {
const postUrl = result['fc:frame:post_url'];
const buttons = [1, 2, 3, 4].map((idx) =>
result[`fc:frame:button:${idx}`]
? {
action: result[`fc:frame:button:${idx}:action`],
action: result[`fc:frame:button:${idx}:action`] || 'post',
label: result[`fc:frame:button:${idx}`],
target: result[`fc:frame:button:${idx}:target`],
target: result[`fc:frame:button:${idx}:target`] || postUrl,
}
: undefined,
);
const image = result['fc:frame:image'];
const imageSrc = result['fc:frame:image'];
const imageAspectRatio = result['fc:frame:image:aspect_ratio'];
const inputText = result['fc:frame:input'];
const input = inputText ? { text: inputText } : undefined;
const postUrl = result['fc:frame:post_url'];
const rawState = result['fc:frame:state'];
const rawRefreshPeriod = result['fc:frame:refresh_period'];
const refreshPeriod = rawRefreshPeriod ? parseInt(rawRefreshPeriod, 10) : undefined;
const state = rawState ? JSON.parse(result['fc:frame:state']) : undefined;
const state = rawState ? JSON.parse(decodeURIComponent(result['fc:frame:state'])) : undefined;

return { buttons: buttons as any, image, input, postUrl, state, refreshPeriod };
return {
buttons: buttons as any,
image: { src: imageSrc, aspectRatio: imageAspectRatio as any },
input,
postUrl,
state,
refreshPeriod,
};
}
4 changes: 3 additions & 1 deletion framegear/utils/parseHtml.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { frameResultToFrameMetadata } from './frameResultToFrameMetadata';
import { vNextSchema } from './validation';

export function parseHtml(html: string) {
Expand Down Expand Up @@ -27,8 +28,9 @@ export function parseHtml(html: string) {

const isValid = vNextSchema.isValidSync(tags);
const errors = aggregateValidationErrors(tags);
const metadata = frameResultToFrameMetadata(tags);

return { isValid, errors, tags };
return { isValid, errors, tags, metadata };
}

function aggregateValidationErrors(tags: Record<string, string>) {
Expand Down

0 comments on commit 1b17de1

Please sign in to comment.