Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add frame state #197

Merged
merged 9 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/frame/getFrameHtmlResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ describe('getFrameHtmlResponse', () => {
},
postUrl: 'https://example.com/api/frame',
refreshPeriod: 10,
state: {
counter: 1,
},
});

expect(html).toBe(`<!DOCTYPE html>
Expand All @@ -40,6 +43,7 @@ describe('getFrameHtmlResponse', () => {
<meta property="fc:frame:input:text" content="Enter a message..." />
<meta property="fc:frame:post_url" content="https://example.com/api/frame" />
<meta property="fc:frame:refresh_period" content="10" />
<meta property="fc:frame:state" content="%7B%22counter%22%3A1%7D" />
taycaldwell marked this conversation as resolved.
Show resolved Hide resolved

</head>
</html>`);
Expand Down Expand Up @@ -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('<meta property="fc:frame" content="vNext" />');
expect(html).toContain('<meta property="fc:frame:button:1" content="button1" />');
expect(html).toContain(
'<meta property="fc:frame:image" content="https://example.com/image.png" />',
);
expect(html).toContain('<meta property="og:image" content="https://example.com/image.png" />');
expect(html).toContain(
'<meta property="fc:frame:post_url" content="https://example.com/api/frame" />',
);
expect(html).not.toContain('fc:frame:state');
});
});

export { getFrameHtmlResponse };
9 changes: 8 additions & 1 deletion src/frame/getFrameHtmlResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -27,6 +28,7 @@ function getFrameHtmlResponse({
post_url,
refreshPeriod,
refresh_period,
state,
}: FrameMetadataHTMLResponse): string {
const imgSrc = typeof image === 'string' ? image : image.src;
const ogImageHtml = ` <meta property="og:image" content="${imgSrc}" />\n`;
Expand All @@ -40,6 +42,11 @@ function getFrameHtmlResponse({
? ` <meta property="fc:frame:input:text" content="${input.text}" />\n`
: '';

// Set the state metadata if it exists.
const stateHtml = state
? ` <meta property="fc:frame:state" content="${encodeURIComponent(JSON.stringify(state))}" />\n`
: '';

// Set the button metadata if it exists.
let buttonsHtml = '';
if (buttons) {
Expand Down Expand Up @@ -76,7 +83,7 @@ function getFrameHtmlResponse({
<meta property="og:description" content="${ogDescription || 'Frame description'}" />
<meta property="og:title" content="${ogTitle || 'Frame title'}" />
<meta property="fc:frame" content="vNext" />
${buttonsHtml}${ogImageHtml}${imageHtml}${inputHtml}${postUrlHtml}${refreshPeriodHtml}
${buttonsHtml}${ogImageHtml}${imageHtml}${inputHtml}${postUrlHtml}${refreshPeriodHtml}${stateHtml}
</head>
</html>`;

Expand Down
2 changes: 2 additions & 0 deletions src/frame/getFrameMessage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('getFrameValidatedMessage', () => {
hash: '0xthisisnotreal',
},
inputText: '',
state: '',
fid: 0,
network: 0,
messageHash: '0xthisisnotreal',
Expand All @@ -72,6 +73,7 @@ describe('getFrameValidatedMessage', () => {
hash: '0xthisisnotreal',
},
inputText: '',
state: '',
fid: 0,
network: 0,
messageHash: '0xthisisnotreal',
Expand Down
21 changes: 21 additions & 0 deletions src/frame/getFrameMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});
});
5 changes: 5 additions & 0 deletions src/frame/getFrameMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand All @@ -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;
Expand Down Expand Up @@ -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;
};
1 change: 1 addition & 0 deletions src/frame/getMockFrameRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/frame/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface FrameData {
fid: number;
messageHash: string;
network: number;
state: string;
timestamp: number;
url: string;
}
Expand All @@ -38,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
Expand Down Expand Up @@ -129,6 +131,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;
};

/**
Expand Down
2 changes: 2 additions & 0 deletions src/utils/neynar/frame/neynarFrameFunctions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('neynar frame functions', () => {
input: {
text: 'test',
},
state: '%7B%22counter%22%3A1%7D',
interactor: {
fid: 1234,
verifications: ['0x00123'],
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/utils/neynar/frame/neynarFrameModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
taycaldwell marked this conversation as resolved.
Show resolved Hide resolved
interactor: {
fid: interactor?.fid,
custody_address: interactor?.custody_address,
Expand Down
1 change: 1 addition & 0 deletions src/utils/neynar/frame/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface NeynarFrameValidationInternalModel {
input: {
text: string;
};
state: string;
url: string;
cast: {
object: string;
Expand Down
Loading