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 all 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
5 changes: 5 additions & 0 deletions .changeset/three-doors-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/onchainkit": minor
---

**feat**: add support for passing `state` to frame server. By @taycaldwell #197
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions site/docs/pages/frame/frame-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export default function HomePage() {
input={{
text: 'Tell me a boat story',
}}
state={{
counter: 1,
}}
postUrl="https://zizzamia.xyz/api/frame"
/>
...
Expand All @@ -54,6 +57,7 @@ export default function HomePage() {
<meta name="fc:frame:image" content="https://zizzamia.xyz/park-3.png" />
<meta name="fc:frame:image:aspect_ratio" content="1:1" />
<meta name="fc:frame:input:text" content="Tell me a boat story" />
<meta name="fc:frame:state" content="%7B%22counter%22%3A1%7D" />
<meta name="fc:frame:post_url" content="https://zizzamia.xyz/api/frame" />
```

Expand Down
2 changes: 2 additions & 0 deletions site/docs/pages/frame/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
```

Expand Down
11 changes: 11 additions & 0 deletions src/frame/components/FrameMetadata.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ describe('FrameMetadata', () => {
expect(meta.container.querySelectorAll('meta').length).toBe(4);
});

it('renders with input', () => {
const meta = render(
<FrameMetadata image="https://example.com/image.png" state={{ counter: 1 }} />,
);
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(
<FrameMetadata
Expand Down
3 changes: 3 additions & 0 deletions src/frame/components/FrameMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type { FrameMetadataReact } from '../types';
* @param {string} props.ogTitle - The Open Graph title.
* @param {string} props.postUrl - The post URL.
* @param {number} props.refreshPeriod - The refresh period.
* @param {object} props.state - The serialized state (e.g. JSON) for the frame.
* @param {React.ComponentType<any> | undefined} props.wrapper - The wrapper component meta tags are rendered in.
* @returns {React.ReactElement} The FrameMetadata component.
*/
Expand All @@ -53,6 +54,7 @@ export function FrameMetadata({
post_url,
refreshPeriod,
refresh_period,
state,
wrapper: Wrapper = Fragment,
}: FrameMetadataReact) {
const button1 = buttons && buttons[0];
Expand All @@ -75,6 +77,7 @@ export function FrameMetadata({
{!!imageSrc && <meta property="fc:frame:image" content={imageSrc} />}
{!!aspectRatio && <meta property="fc:frame:image:aspect_ratio" content={aspectRatio} />}
{!!input && <meta property="fc:frame:input:text" content={input.text} />}
{!!state && <meta property="fc:frame:state" content={encodeURIComponent(JSON.stringify(state))} />}

{!!button1 && <meta property="fc:frame:button:1" content={button1.label} />}
{!!(button1 && !!button1.action) && (
Expand Down
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
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;
};
2 changes: 2 additions & 0 deletions src/frame/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
Loading