From a74518b2fda562177edccd21e6f0a0cc849a3375 Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 13:55:20 -0800 Subject: [PATCH 1/9] Add frame state --- src/frame/getFrameHtmlResponse.test.ts | 23 +++++++++++++++++++++++ src/frame/getFrameHtmlResponse.ts | 9 ++++++++- src/frame/getFrameMetadata.test.ts | 21 +++++++++++++++++++++ src/frame/getFrameMetadata.ts | 5 +++++ src/frame/types.ts | 2 ++ 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/frame/getFrameHtmlResponse.test.ts b/src/frame/getFrameHtmlResponse.test.ts index 39d0a24540..dbcf44b0c8 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..247edb91c7 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..1e9de0ef14 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; }; /** From 3a2161240a74e934bd94ad15fffb0d9c58ba6129 Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 14:36:19 -0800 Subject: [PATCH 2/9] Add state field to FrameData --- src/frame/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frame/types.ts b/src/frame/types.ts index a182f51240..b46f684514 100644 --- a/src/frame/types.ts +++ b/src/frame/types.ts @@ -15,6 +15,7 @@ export interface FrameData { fid: number; messageHash: string; network: number; + state?: string; timestamp: number; url: string; } From f852d0691cbd375fb60e8be86e8e32032898374e Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 14:42:13 -0800 Subject: [PATCH 3/9] format --- src/frame/getFrameHtmlResponse.test.ts | 2 +- src/frame/getFrameHtmlResponse.ts | 4 ++-- src/frame/getFrameMetadata.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frame/getFrameHtmlResponse.test.ts b/src/frame/getFrameHtmlResponse.test.ts index dbcf44b0c8..5fd0624b47 100644 --- a/src/frame/getFrameHtmlResponse.test.ts +++ b/src/frame/getFrameHtmlResponse.test.ts @@ -19,7 +19,7 @@ describe('getFrameHtmlResponse', () => { postUrl: 'https://example.com/api/frame', refreshPeriod: 10, state: { - counter: 1 + counter: 1, }, }); diff --git a/src/frame/getFrameHtmlResponse.ts b/src/frame/getFrameHtmlResponse.ts index 247edb91c7..7950c32601 100644 --- a/src/frame/getFrameHtmlResponse.ts +++ b/src/frame/getFrameHtmlResponse.ts @@ -44,8 +44,8 @@ function getFrameHtmlResponse({ // Set the state metadata if it exists. const stateHtml = state - ? ` \n` - : ''; + ? ` \n` + : ''; // Set the button metadata if it exists. let buttonsHtml = ''; diff --git a/src/frame/getFrameMetadata.test.ts b/src/frame/getFrameMetadata.test.ts index 1e9de0ef14..317f788dd0 100644 --- a/src/frame/getFrameMetadata.test.ts +++ b/src/frame/getFrameMetadata.test.ts @@ -196,7 +196,7 @@ describe('getFrameMetadata', () => { postUrl: 'post_url', refreshPeriod: 10, state: { - counter: 1 + counter: 1, }, }), ).toEqual({ From 241c41f84f49b9f7ca536c71f984c0f96692ae48 Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 16:29:29 -0800 Subject: [PATCH 4/9] dasdsa --- src/frame/getFrameMessage.test.ts | 2 ++ src/frame/getMockFrameRequest.ts | 1 + src/frame/types.ts | 3 ++- src/utils/neynar/frame/neynarFrameFunctions.test.ts | 2 ++ src/utils/neynar/frame/neynarFrameModels.ts | 1 + src/utils/neynar/frame/types.ts | 1 + 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/frame/getFrameMessage.test.ts b/src/frame/getFrameMessage.test.ts index acc90f1f58..2891cd3f83 100644 --- a/src/frame/getFrameMessage.test.ts +++ b/src/frame/getFrameMessage.test.ts @@ -46,6 +46,7 @@ describe('getFrameValidatedMessage', () => { hash: '0xthisisnotreal', }, inputText: '', + state: '', fid: 0, network: 0, messageHash: '0xthisisnotreal', @@ -72,6 +73,7 @@ describe('getFrameValidatedMessage', () => { hash: '0xthisisnotreal', }, inputText: '', + state: '', fid: 0, network: 0, messageHash: '0xthisisnotreal', diff --git a/src/frame/getMockFrameRequest.ts b/src/frame/getMockFrameRequest.ts index 92fa345544..fc4b0dc6b9 100644 --- a/src/frame/getMockFrameRequest.ts +++ b/src/frame/getMockFrameRequest.ts @@ -16,6 +16,7 @@ function getMockFrameRequest( mockFrameData: { button: request.untrustedData.buttonIndex, input: request.untrustedData.inputText, + state: request.untrustedData.state, following: !!options?.following, interactor: { fid: options?.interactor?.fid || 0, diff --git a/src/frame/types.ts b/src/frame/types.ts index b46f684514..10b1fb69b5 100644 --- a/src/frame/types.ts +++ b/src/frame/types.ts @@ -15,7 +15,7 @@ export interface FrameData { fid: number; messageHash: string; network: number; - state?: string; + state: string; timestamp: number; url: string; } @@ -39,6 +39,7 @@ export interface FrameValidationData { button: number; // Number of the button clicked following: boolean; // Indicates if the viewer clicking the frame follows the cast author input: string; // Text input from the viewer typing in the frame + state: string; interactor: { fid: number; // Viewer Farcaster ID custody_address: string; // Viewer custody address diff --git a/src/utils/neynar/frame/neynarFrameFunctions.test.ts b/src/utils/neynar/frame/neynarFrameFunctions.test.ts index 7d4feb54d2..510d29e594 100644 --- a/src/utils/neynar/frame/neynarFrameFunctions.test.ts +++ b/src/utils/neynar/frame/neynarFrameFunctions.test.ts @@ -30,6 +30,7 @@ describe('neynar frame functions', () => { input: { text: 'test', }, + state: '%7B%22counter%22%3A1%7D', interactor: { fid: 1234, verifications: ['0x00123'], @@ -48,6 +49,7 @@ describe('neynar frame functions', () => { expect(resp?.recasted).toEqual(mockedResponse.action.cast.viewer_context.recasted); expect(resp?.button).toEqual(mockedResponse.action.tapped_button.index); expect(resp?.input).toEqual(mockedResponse.action.input.text); + expect(resp?.state).toEqual(mockedResponse.action.state); expect(resp?.interactor?.fid).toEqual(mockedResponse.action.interactor.fid); expect(resp?.interactor?.verified_accounts).toEqual( mockedResponse.action.interactor.verifications, diff --git a/src/utils/neynar/frame/neynarFrameModels.ts b/src/utils/neynar/frame/neynarFrameModels.ts index a40540c138..dbc4620ef0 100644 --- a/src/utils/neynar/frame/neynarFrameModels.ts +++ b/src/utils/neynar/frame/neynarFrameModels.ts @@ -22,6 +22,7 @@ export function convertToNeynarResponseModel(data: any): FrameValidationData | u button: action?.tapped_button?.index, following: action?.interactor?.viewer_context?.following, input: action?.input?.text, + state: action?.state, interactor: { fid: interactor?.fid, custody_address: interactor?.custody_address, diff --git a/src/utils/neynar/frame/types.ts b/src/utils/neynar/frame/types.ts index f03e6bd329..93a1c08b22 100644 --- a/src/utils/neynar/frame/types.ts +++ b/src/utils/neynar/frame/types.ts @@ -37,6 +37,7 @@ export interface NeynarFrameValidationInternalModel { input: { text: string; }; + state: string; url: string; cast: { object: string; From a7cddcd06f4a25315aa6a5c463522e2a798a91c6 Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 17:20:49 -0800 Subject: [PATCH 5/9] revert changes --- src/frame/getFrameMessage.test.ts | 2 -- src/frame/getMockFrameRequest.ts | 1 - src/frame/types.ts | 2 -- src/utils/neynar/frame/neynarFrameFunctions.test.ts | 2 -- src/utils/neynar/frame/neynarFrameModels.ts | 1 - 5 files changed, 8 deletions(-) diff --git a/src/frame/getFrameMessage.test.ts b/src/frame/getFrameMessage.test.ts index 2891cd3f83..acc90f1f58 100644 --- a/src/frame/getFrameMessage.test.ts +++ b/src/frame/getFrameMessage.test.ts @@ -46,7 +46,6 @@ describe('getFrameValidatedMessage', () => { hash: '0xthisisnotreal', }, inputText: '', - state: '', fid: 0, network: 0, messageHash: '0xthisisnotreal', @@ -73,7 +72,6 @@ describe('getFrameValidatedMessage', () => { hash: '0xthisisnotreal', }, inputText: '', - state: '', fid: 0, network: 0, messageHash: '0xthisisnotreal', diff --git a/src/frame/getMockFrameRequest.ts b/src/frame/getMockFrameRequest.ts index fc4b0dc6b9..92fa345544 100644 --- a/src/frame/getMockFrameRequest.ts +++ b/src/frame/getMockFrameRequest.ts @@ -16,7 +16,6 @@ function getMockFrameRequest( mockFrameData: { button: request.untrustedData.buttonIndex, input: request.untrustedData.inputText, - state: request.untrustedData.state, following: !!options?.following, interactor: { fid: options?.interactor?.fid || 0, diff --git a/src/frame/types.ts b/src/frame/types.ts index 10b1fb69b5..a182f51240 100644 --- a/src/frame/types.ts +++ b/src/frame/types.ts @@ -15,7 +15,6 @@ export interface FrameData { fid: number; messageHash: string; network: number; - state: string; timestamp: number; url: string; } @@ -39,7 +38,6 @@ export interface FrameValidationData { button: number; // Number of the button clicked following: boolean; // Indicates if the viewer clicking the frame follows the cast author input: string; // Text input from the viewer typing in the frame - state: string; interactor: { fid: number; // Viewer Farcaster ID custody_address: string; // Viewer custody address diff --git a/src/utils/neynar/frame/neynarFrameFunctions.test.ts b/src/utils/neynar/frame/neynarFrameFunctions.test.ts index 510d29e594..7d4feb54d2 100644 --- a/src/utils/neynar/frame/neynarFrameFunctions.test.ts +++ b/src/utils/neynar/frame/neynarFrameFunctions.test.ts @@ -30,7 +30,6 @@ describe('neynar frame functions', () => { input: { text: 'test', }, - state: '%7B%22counter%22%3A1%7D', interactor: { fid: 1234, verifications: ['0x00123'], @@ -49,7 +48,6 @@ describe('neynar frame functions', () => { expect(resp?.recasted).toEqual(mockedResponse.action.cast.viewer_context.recasted); expect(resp?.button).toEqual(mockedResponse.action.tapped_button.index); expect(resp?.input).toEqual(mockedResponse.action.input.text); - expect(resp?.state).toEqual(mockedResponse.action.state); expect(resp?.interactor?.fid).toEqual(mockedResponse.action.interactor.fid); expect(resp?.interactor?.verified_accounts).toEqual( mockedResponse.action.interactor.verifications, diff --git a/src/utils/neynar/frame/neynarFrameModels.ts b/src/utils/neynar/frame/neynarFrameModels.ts index dbc4620ef0..a40540c138 100644 --- a/src/utils/neynar/frame/neynarFrameModels.ts +++ b/src/utils/neynar/frame/neynarFrameModels.ts @@ -22,7 +22,6 @@ export function convertToNeynarResponseModel(data: any): FrameValidationData | u button: action?.tapped_button?.index, following: action?.interactor?.viewer_context?.following, input: action?.input?.text, - state: action?.state, interactor: { fid: interactor?.fid, custody_address: interactor?.custody_address, From 3cb61add8ff2b15dea325455e63b687e0503ee4e Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 17:21:45 -0800 Subject: [PATCH 6/9] revert neynar change --- src/utils/neynar/frame/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/neynar/frame/types.ts b/src/utils/neynar/frame/types.ts index 93a1c08b22..f03e6bd329 100644 --- a/src/utils/neynar/frame/types.ts +++ b/src/utils/neynar/frame/types.ts @@ -37,7 +37,6 @@ export interface NeynarFrameValidationInternalModel { input: { text: string; }; - state: string; url: string; cast: { object: string; From fa4e900e8be353733e24f09075bcf802b84609e3 Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 17:54:26 -0800 Subject: [PATCH 7/9] Update FrameMetadata and docs --- site/docs/pages/frame/frame-metadata.mdx | 4 ++++ site/docs/pages/frame/types.mdx | 2 ++ src/frame/components/FrameMetadata.test.tsx | 11 +++++++++++ src/frame/components/FrameMetadata.tsx | 3 +++ 4 files changed, 20 insertions(+) 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) && ( From 16121ac3f9b950761b5bc98f079e3a41d1872d77 Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 17:57:34 -0800 Subject: [PATCH 8/9] Update changelog --- .changeset/three-doors-know.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/three-doors-know.md diff --git a/.changeset/three-doors-know.md b/.changeset/three-doors-know.md new file mode 100644 index 0000000000..61e5f96278 --- /dev/null +++ b/.changeset/three-doors-know.md @@ -0,0 +1,5 @@ +--- +"@coinbase/onchainkit": minor +--- + +Add support for passing state to frame server From 07c7aaed193b77c87a02eff21fa83b1883c65d6d Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 18:01:22 -0800 Subject: [PATCH 9/9] Update changelog --- .changeset/three-doors-know.md | 2 +- CHANGELOG.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.changeset/three-doors-know.md b/.changeset/three-doors-know.md index 61e5f96278..7984fedfa5 100644 --- a/.changeset/three-doors-know.md +++ b/.changeset/three-doors-know.md @@ -2,4 +2,4 @@ "@coinbase/onchainkit": minor --- -Add support for passing state to frame server +**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