From 378988986beaa3df8be5c854aaa62fd7b64a79d6 Mon Sep 17 00:00:00 2001 From: Christopher Nascone Date: Wed, 6 Mar 2024 20:55:57 -0500 Subject: [PATCH 1/6] refactor: Frame uses FrameMetadata --- framegear/components/Frame/Frame.tsx | 62 ++++++++----------- framegear/utils/fetchFrame.ts | 7 ++- framegear/utils/frameResultToFrameMetadata.ts | 22 +++++-- framegear/utils/postFrame.ts | 7 ++- 4 files changed, 55 insertions(+), 43 deletions(-) diff --git a/framegear/components/Frame/Frame.tsx b/framegear/components/Frame/Frame.tsx index a361f64a85..f30d246904 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); @@ -21,33 +22,11 @@ export function Frame() { return ; } -function ValidFrame({ tags }: { tags: Record }) { +function ValidFrame({ tags }: { tags: 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 } = tags; + const imageAspectRatioClassname = + tags.image.aspectRatio === '1:1' ? 'aspect-square' : 'aspect-[1.91/1]'; const handleInputChange = useCallback( (e: ChangeEvent) => setInputText(e.target.value), @@ -59,7 +38,7 @@ function ValidFrame({ tags }: { tags: Record }) { {/* eslint-disable-next-line @next/next/no-img-element */}
@@ -67,15 +46,20 @@ function ValidFrame({ tags }: { tags: Record }) { )}
- {buttons.map((button) => + {buttons?.map((button, index) => button ? ( - - {button.value} + + {button.label} ) : null, )} @@ -97,7 +81,9 @@ function PlaceholderFrame() {
- Get Started + + Get Started +
); @@ -106,10 +92,11 @@ function PlaceholderFrame() { function FrameButton({ children, button, + index, inputText, }: 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; }>) { const { openModal } = useRedirectModal(); @@ -121,8 +108,8 @@ 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!, // TODO: make these user-input-driven castId: { fid: 0, @@ -133,6 +120,7 @@ function FrameButton({ messageHash: '0xthisisnotreal', network: 0, timestamp: 0, + state: '', }); // TODO: handle when result is not defined if (result) { @@ -152,7 +140,7 @@ function FrameButton({ openModal(onConfirm); } // TODO: implement other actions (mint, etc.) - }, [button?.action, button?.index, button?.target, inputText, openModal, setResults]); + }, [button?.action, button?.target, index, inputText, openModal, setResults]); const buttonIcon = useMemo(() => { switch (button?.action) { diff --git a/framegear/utils/fetchFrame.ts b/framegear/utils/fetchFrame.ts index 5f0f4e192b..23103b5b9e 100644 --- a/framegear/utils/fetchFrame.ts +++ b/framegear/utils/fetchFrame.ts @@ -1,3 +1,4 @@ +import { frameResultToFrameMetadata } from './frameResultToFrameMetadata'; import { parseHtml } from './parseHtml'; export async function fetchFrame(url: string) { @@ -11,5 +12,9 @@ export async function fetchFrame(url: string) { const json = (await response.json()) as { html: string }; const html = json.html; - return parseHtml(html); + const parsedHtml = parseHtml(html); + return { + ...parsedHtml, + tags: frameResultToFrameMetadata(parsedHtml.tags), + }; } diff --git a/framegear/utils/frameResultToFrameMetadata.ts b/framegear/utils/frameResultToFrameMetadata.ts index e4f29b90d4..5a5d1d15f5 100644 --- a/framegear/utils/frameResultToFrameMetadata.ts +++ b/framegear/utils/frameResultToFrameMetadata.ts @@ -1,6 +1,12 @@ -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 buttons = [1, 2, 3, 4].map((idx) => result[`fc:frame:button:${idx}`] ? { @@ -10,7 +16,8 @@ export function frameResultToFrameMetadata(result: Record): Fram } : 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']; @@ -19,5 +26,12 @@ export function frameResultToFrameMetadata(result: Record): Fram const refreshPeriod = rawRefreshPeriod ? parseInt(rawRefreshPeriod, 10) : undefined; const state = rawState ? JSON.parse(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/postFrame.ts b/framegear/utils/postFrame.ts index cc99b5b8a4..050d9e64a4 100644 --- a/framegear/utils/postFrame.ts +++ b/framegear/utils/postFrame.ts @@ -1,5 +1,6 @@ import { FrameRequest, MockFrameRequestOptions } from '@coinbase/onchainkit'; import { parseHtml } from './parseHtml'; +import { frameResultToFrameMetadata } from './frameResultToFrameMetadata'; type FrameData = FrameRequest['untrustedData']; @@ -23,5 +24,9 @@ export async function postFrame(frameData: FrameData, options?: MockFrameRequest } const html = json.html; - return parseHtml(html); + const parsedHtml = parseHtml(html); + return { + ...parsedHtml, + tags: frameResultToFrameMetadata(parsedHtml.tags), + }; } From 4f099f0f0ab65480316ebc57cbf2ddb969c51278 Mon Sep 17 00:00:00 2001 From: Christopher Nascone Date: Wed, 6 Mar 2024 21:04:08 -0500 Subject: [PATCH 2/6] refactor: include legacy tags for validation --- framegear/components/Frame/Frame.tsx | 8 ++++---- framegear/utils/parseHtml.ts | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/framegear/components/Frame/Frame.tsx b/framegear/components/Frame/Frame.tsx index f30d246904..6ed8b5356f 100644 --- a/framegear/components/Frame/Frame.tsx +++ b/framegear/components/Frame/Frame.tsx @@ -19,14 +19,14 @@ export function Frame() { return ; } - return ; + return ; } -function ValidFrame({ tags }: { tags: FrameMetadataWithImageObject }) { +function ValidFrame({ metadata }: { metadata: FrameMetadataWithImageObject }) { const [inputText, setInputText] = useState(''); - const { image, input, buttons } = tags; + const { image, input, buttons } = metadata; const imageAspectRatioClassname = - tags.image.aspectRatio === '1:1' ? 'aspect-square' : 'aspect-[1.91/1]'; + metadata.image.aspectRatio === '1:1' ? 'aspect-square' : 'aspect-[1.91/1]'; const handleInputChange = useCallback( (e: ChangeEvent) => setInputText(e.target.value), 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) { From cdbeaf530a577e93e84d1e8771e849ec850d257c Mon Sep 17 00:00:00 2001 From: Christopher Nascone Date: Wed, 6 Mar 2024 21:04:24 -0500 Subject: [PATCH 3/6] refactor: do this up a level --- framegear/utils/fetchFrame.ts | 7 +------ framegear/utils/postFrame.ts | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/framegear/utils/fetchFrame.ts b/framegear/utils/fetchFrame.ts index 23103b5b9e..5f0f4e192b 100644 --- a/framegear/utils/fetchFrame.ts +++ b/framegear/utils/fetchFrame.ts @@ -1,4 +1,3 @@ -import { frameResultToFrameMetadata } from './frameResultToFrameMetadata'; import { parseHtml } from './parseHtml'; export async function fetchFrame(url: string) { @@ -12,9 +11,5 @@ export async function fetchFrame(url: string) { const json = (await response.json()) as { html: string }; const html = json.html; - const parsedHtml = parseHtml(html); - return { - ...parsedHtml, - tags: frameResultToFrameMetadata(parsedHtml.tags), - }; + return parseHtml(html); } diff --git a/framegear/utils/postFrame.ts b/framegear/utils/postFrame.ts index 050d9e64a4..cc99b5b8a4 100644 --- a/framegear/utils/postFrame.ts +++ b/framegear/utils/postFrame.ts @@ -1,6 +1,5 @@ import { FrameRequest, MockFrameRequestOptions } from '@coinbase/onchainkit'; import { parseHtml } from './parseHtml'; -import { frameResultToFrameMetadata } from './frameResultToFrameMetadata'; type FrameData = FrameRequest['untrustedData']; @@ -24,9 +23,5 @@ export async function postFrame(frameData: FrameData, options?: MockFrameRequest } const html = json.html; - const parsedHtml = parseHtml(html); - return { - ...parsedHtml, - tags: frameResultToFrameMetadata(parsedHtml.tags), - }; + return parseHtml(html); } From 95492d27eae21e2922eb8722c4f4bc54eddf60ea Mon Sep 17 00:00:00 2001 From: Christopher Nascone Date: Wed, 6 Mar 2024 21:18:19 -0500 Subject: [PATCH 4/6] fix: state handling --- framegear/components/Frame/Frame.tsx | 13 ++++++------- framegear/utils/frameResultToFrameMetadata.ts | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/framegear/components/Frame/Frame.tsx b/framegear/components/Frame/Frame.tsx index 6ed8b5356f..7d426acd28 100644 --- a/framegear/components/Frame/Frame.tsx +++ b/framegear/components/Frame/Frame.tsx @@ -15,10 +15,6 @@ export function Frame() { const latestFrame = results[results.length - 1]; - if (!latestFrame.isValid) { - return ; - } - return ; } @@ -58,6 +54,7 @@ function ValidFrame({ metadata }: { metadata: FrameMetadataWithImageObject }) { key={button.label} index={index + 1} button={button} + state={metadata.state} > {button.label}
@@ -81,7 +78,7 @@ function PlaceholderFrame() {
- + Get Started
@@ -94,10 +91,12 @@ function FrameButton({ button, index, inputText, + state, }: PropsWithChildren<{ button?: NonNullable[0]; index: number; inputText: string; + state: any; }>) { const { openModal } = useRedirectModal(); const [isLoading, setIsLoading] = useState(false); @@ -110,6 +109,7 @@ function FrameButton({ const result = await postFrame({ buttonIndex: index, url: button.target!, + state: JSON.stringify(state), // TODO: make these user-input-driven castId: { fid: 0, @@ -120,7 +120,6 @@ function FrameButton({ messageHash: '0xthisisnotreal', network: 0, timestamp: 0, - state: '', }); // TODO: handle when result is not defined if (result) { @@ -140,7 +139,7 @@ function FrameButton({ openModal(onConfirm); } // TODO: implement other actions (mint, etc.) - }, [button?.action, button?.target, index, inputText, openModal, setResults]); + }, [button, index, inputText, openModal, setResults, state]); const buttonIcon = useMemo(() => { switch (button?.action) { diff --git a/framegear/utils/frameResultToFrameMetadata.ts b/framegear/utils/frameResultToFrameMetadata.ts index 5a5d1d15f5..0389dad84c 100644 --- a/framegear/utils/frameResultToFrameMetadata.ts +++ b/framegear/utils/frameResultToFrameMetadata.ts @@ -7,12 +7,13 @@ export type FrameMetadataWithImageObject = FrameMetadataType & { 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, ); @@ -20,11 +21,10 @@ export function frameResultToFrameMetadata( 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, From 28de28dbfda630895b1b9bc3db96247162ae3692 Mon Sep 17 00:00:00 2001 From: Christopher Nascone Date: Wed, 6 Mar 2024 21:19:16 -0500 Subject: [PATCH 5/6] fix: doc --- framegear/components/Frame/Frame.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/framegear/components/Frame/Frame.tsx b/framegear/components/Frame/Frame.tsx index 7d426acd28..32653ec4ab 100644 --- a/framegear/components/Frame/Frame.tsx +++ b/framegear/components/Frame/Frame.tsx @@ -70,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 ; } From ec5cbf3f842a585986fe5585a38581697f6cfa61 Mon Sep 17 00:00:00 2001 From: Christopher Nascone Date: Wed, 6 Mar 2024 21:27:29 -0500 Subject: [PATCH 6/6] fix: tests --- framegear/utils/frameResultToFrameMetadata.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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,