From a74518b2fda562177edccd21e6f0a0cc849a3375 Mon Sep 17 00:00:00 2001 From: Taylor Caldwell Date: Mon, 26 Feb 2024 13:55:20 -0800 Subject: [PATCH] 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; }; /**