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: Frame now uses FrameMetadata instead of string record #232

Merged
merged 6 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this because right now it's more useful to fully render the frame even if it is not 100% correct

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
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
Loading