diff --git a/.changeset/three-doors-know.md b/.changeset/three-doors-know.md new file mode 100644 index 0000000000..7984fedfa5 --- /dev/null +++ b/.changeset/three-doors-know.md @@ -0,0 +1,5 @@ +--- +"@coinbase/onchainkit": minor +--- + +**feat**: add support for passing `state` to frame server. By @taycaldwell #197 diff --git a/CHANGELOG.md b/CHANGELOG.md index dab71e0704..5fda9297ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog + +## 0.10.0 + +### Minor Changes + +- **feat**: add support for passing `state` to frame server. By @taycaldwell #197 + ## 0.9.4 ### Patch Changes diff --git a/site/docs/pages/frame/frame-metadata.mdx b/site/docs/pages/frame/frame-metadata.mdx index 9214908a51..ac659d5592 100644 --- a/site/docs/pages/frame/frame-metadata.mdx +++ b/site/docs/pages/frame/frame-metadata.mdx @@ -34,6 +34,9 @@ export default function HomePage() { input={{ text: 'Tell me a boat story', }} + state={{ + counter: 1, + }} postUrl="https://zizzamia.xyz/api/frame" /> ... @@ -54,6 +57,7 @@ export default function HomePage() { + ``` diff --git a/site/docs/pages/frame/types.mdx b/site/docs/pages/frame/types.mdx index 48d2561767..eda52c857f 100644 --- a/site/docs/pages/frame/types.mdx +++ b/site/docs/pages/frame/types.mdx @@ -68,6 +68,8 @@ type FrameMetadataType = { postUrl?: string; // A period in seconds at which the app should expect the image to update. refreshPeriod?: number; + // A string containing serialized state (e.g. JSON) passed to the frame server. + state?: object; }; ``` diff --git a/src/frame/components/FrameMetadata.test.tsx b/src/frame/components/FrameMetadata.test.tsx index b408e9a2b7..ab938e9129 100644 --- a/src/frame/components/FrameMetadata.test.tsx +++ b/src/frame/components/FrameMetadata.test.tsx @@ -52,6 +52,17 @@ describe('FrameMetadata', () => { expect(meta.container.querySelectorAll('meta').length).toBe(4); }); + it('renders with input', () => { + const meta = render( + , + ); + expect(meta.container.querySelector('meta[property="fc:frame:state"]')).not.toBeNull(); + expect( + meta.container.querySelector('meta[property="fc:frame:state"]')?.getAttribute('content'), + ).toBe('%7B%22counter%22%3A1%7D'); + expect(meta.container.querySelectorAll('meta').length).toBe(4); + }); + it('renders with two basic buttons', () => { const meta = render( | undefined} props.wrapper - The wrapper component meta tags are rendered in. * @returns {React.ReactElement} The FrameMetadata component. */ @@ -53,6 +54,7 @@ export function FrameMetadata({ post_url, refreshPeriod, refresh_period, + state, wrapper: Wrapper = Fragment, }: FrameMetadataReact) { const button1 = buttons && buttons[0]; @@ -75,6 +77,7 @@ export function FrameMetadata({ {!!imageSrc && } {!!aspectRatio && } {!!input && } + {!!state && } {!!button1 && } {!!(button1 && !!button1.action) && ( diff --git a/src/frame/getFrameHtmlResponse.test.ts b/src/frame/getFrameHtmlResponse.test.ts index 39d0a24540..5fd0624b47 100644 --- a/src/frame/getFrameHtmlResponse.test.ts +++ b/src/frame/getFrameHtmlResponse.test.ts @@ -18,6 +18,9 @@ describe('getFrameHtmlResponse', () => { }, postUrl: 'https://example.com/api/frame', refreshPeriod: 10, + state: { + counter: 1, + }, }); expect(html).toBe(` @@ -40,6 +43,7 @@ describe('getFrameHtmlResponse', () => { + `); @@ -291,6 +295,25 @@ describe('getFrameHtmlResponse', () => { expect(html).not.toContain('fc:frame:button:4:action'); expect(html).not.toContain('fc:frame:button:4:target'); }); + + it('should handle no state', () => { + const html = getFrameHtmlResponse({ + buttons: [{ label: 'button1' }], + image: 'https://example.com/image.png', + postUrl: 'https://example.com/api/frame', + }); + + expect(html).toContain(''); + expect(html).toContain(''); + expect(html).toContain( + '', + ); + expect(html).toContain(''); + expect(html).toContain( + '', + ); + expect(html).not.toContain('fc:frame:state'); + }); }); export { getFrameHtmlResponse }; diff --git a/src/frame/getFrameHtmlResponse.ts b/src/frame/getFrameHtmlResponse.ts index 1428e6a7e4..7950c32601 100644 --- a/src/frame/getFrameHtmlResponse.ts +++ b/src/frame/getFrameHtmlResponse.ts @@ -15,6 +15,7 @@ type FrameMetadataHTMLResponse = FrameMetadataType & { * @param ogTitle: The Open Graph title for the frame. * @param postUrl: The URL to post the frame to. * @param refreshPeriod: The refresh period for the image used. + * @param state: The serialized state (e.g. JSON) for the frame. * @returns An HTML string containing metadata for the frame. */ function getFrameHtmlResponse({ @@ -27,6 +28,7 @@ function getFrameHtmlResponse({ post_url, refreshPeriod, refresh_period, + state, }: FrameMetadataHTMLResponse): string { const imgSrc = typeof image === 'string' ? image : image.src; const ogImageHtml = ` \n`; @@ -40,6 +42,11 @@ function getFrameHtmlResponse({ ? ` \n` : ''; + // Set the state metadata if it exists. + const stateHtml = state + ? ` \n` + : ''; + // Set the button metadata if it exists. let buttonsHtml = ''; if (buttons) { @@ -76,7 +83,7 @@ function getFrameHtmlResponse({ -${buttonsHtml}${ogImageHtml}${imageHtml}${inputHtml}${postUrlHtml}${refreshPeriodHtml} +${buttonsHtml}${ogImageHtml}${imageHtml}${inputHtml}${postUrlHtml}${refreshPeriodHtml}${stateHtml} `; diff --git a/src/frame/getFrameMetadata.test.ts b/src/frame/getFrameMetadata.test.ts index 832dddadbe..317f788dd0 100644 --- a/src/frame/getFrameMetadata.test.ts +++ b/src/frame/getFrameMetadata.test.ts @@ -187,4 +187,25 @@ describe('getFrameMetadata', () => { 'fc:frame:image': 'image', }); }); + + it('should return the correct metadata with state', () => { + expect( + getFrameMetadata({ + buttons: [{ label: 'button1' }], + image: 'image', + postUrl: 'post_url', + refreshPeriod: 10, + state: { + counter: 1, + }, + }), + ).toEqual({ + 'fc:frame': 'vNext', + 'fc:frame:button:1': 'button1', + 'fc:frame:image': 'image', + 'fc:frame:post_url': 'post_url', + 'fc:frame:refresh_period': '10', + 'fc:frame:state': '%7B%22counter%22%3A1%7D', + }); + }); }); diff --git a/src/frame/getFrameMetadata.ts b/src/frame/getFrameMetadata.ts index 45a2c4791c..ab97512984 100644 --- a/src/frame/getFrameMetadata.ts +++ b/src/frame/getFrameMetadata.ts @@ -7,6 +7,7 @@ import { FrameMetadataResponse, FrameMetadataType } from './types'; * @param input: The text input to use for the frame. * @param postUrl: The URL to post the frame to. * @param refreshPeriod: The refresh period for the image used. + * @param state: The serialized state (e.g. JSON) for the frame. * @returns The metadata for the frame. */ export const getFrameMetadata = function ({ @@ -17,6 +18,7 @@ export const getFrameMetadata = function ({ post_url, refreshPeriod, refresh_period, + state, }: FrameMetadataType): FrameMetadataResponse { const postUrlToUse = postUrl || post_url; const refreshPeriodToUse = refreshPeriod || refresh_period; @@ -52,5 +54,8 @@ export const getFrameMetadata = function ({ if (refreshPeriodToUse) { metadata['fc:frame:refresh_period'] = refreshPeriodToUse.toString(); } + if (state) { + metadata['fc:frame:state'] = encodeURIComponent(JSON.stringify(state)); + } return metadata; }; diff --git a/src/frame/types.ts b/src/frame/types.ts index 8083f2c1c8..a182f51240 100644 --- a/src/frame/types.ts +++ b/src/frame/types.ts @@ -129,6 +129,8 @@ export type FrameMetadataType = { refresh_period?: number; // A period in seconds at which the app should expect the image to update. refreshPeriod?: number; + // A string containing serialized state (e.g. JSON) passed to the frame server. + state?: object; }; /**