From 1b17de13a8234a4543dfd54a55333d2ea4a4b70f Mon Sep 17 00:00:00 2001 From: Christopher Nascone Date: Thu, 7 Mar 2024 00:38:50 -0500 Subject: [PATCH] feat: Frame now uses FrameMetadata instead of string record (#232) --- framegear/components/Frame/Frame.tsx | 72 ++++++++----------- .../utils/frameResultToFrameMetadata.test.ts | 4 +- framegear/utils/frameResultToFrameMetadata.ts | 30 +++++--- framegear/utils/parseHtml.ts | 4 +- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/framegear/components/Frame/Frame.tsx b/framegear/components/Frame/Frame.tsx index a361f64a85..32653ec4ab 100644 --- a/framegear/components/Frame/Frame.tsx +++ b/framegear/components/Frame/Frame.tsx @@ -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); @@ -14,40 +15,14 @@ export function Frame() { const latestFrame = results[results.length - 1]; - if (!latestFrame.isValid) { - return ; - } - - return ; + return ; } -function ValidFrame({ tags }: { tags: Record }) { +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) => setInputText(e.target.value), @@ -59,7 +34,7 @@ function ValidFrame({ tags }: { tags: Record }) { {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -67,15 +42,21 @@ function ValidFrame({ tags }: { tags: Record }) { )}
- {buttons.map((button) => + {buttons?.map((button, index) => button ? ( - - {button.value} + + {button.label} ) : null, )} @@ -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 ; } @@ -97,7 +79,9 @@ function PlaceholderFrame() {
- Get Started + + Get Started +
); @@ -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[0]; + index: number; inputText: string; + state: any; }>) { const { openModal } = useRedirectModal(); const [isLoading, setIsLoading] = useState(false); @@ -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, @@ -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) { diff --git a/framegear/utils/frameResultToFrameMetadata.test.ts b/framegear/utils/frameResultToFrameMetadata.test.ts index f3f7297deb..a1cde20bfe 100644 --- a/framegear/utils/frameResultToFrameMetadata.test.ts +++ b/framegear/utils/frameResultToFrameMetadata.test.ts @@ -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' }, @@ -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, diff --git a/framegear/utils/frameResultToFrameMetadata.ts b/framegear/utils/frameResultToFrameMetadata.ts index e4f29b90d4..0389dad84c 100644 --- a/framegear/utils/frameResultToFrameMetadata.ts +++ b/framegear/utils/frameResultToFrameMetadata.ts @@ -1,23 +1,37 @@ -import { FrameMetadataType } from '@coinbase/onchainkit'; +import { FrameImageMetadata, FrameMetadataType } from '@coinbase/onchainkit'; -export function frameResultToFrameMetadata(result: Record): FrameMetadataType { +export type FrameMetadataWithImageObject = FrameMetadataType & { + image: FrameImageMetadata; +}; + +export function frameResultToFrameMetadata( + result: Record, +): 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, + }; } diff --git a/framegear/utils/parseHtml.ts b/framegear/utils/parseHtml.ts index 0913ed88b8..58393fcada 100644 --- a/framegear/utils/parseHtml.ts +++ b/framegear/utils/parseHtml.ts @@ -1,3 +1,4 @@ +import { frameResultToFrameMetadata } from './frameResultToFrameMetadata'; import { vNextSchema } from './validation'; export function parseHtml(html: string) { @@ -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) {