Skip to content

Commit

Permalink
feat: init Open Frame spec support (coinbase#285)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zizzamia authored Apr 12, 2024
1 parent c7b0562 commit b8aa317
Show file tree
Hide file tree
Showing 13 changed files with 185 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/famous-glasses-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@coinbase/onchainkit': minor
---

- **feat**: init Open Frame spec support. By @zizzamia @daria-github @neekolas #285
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Patch Changes

- b795268: - **feat**: exposed the `getName` and `getAvatar` utilities to assist in retrieving name and avatar identity information. These utilities come in handy when working with Next.js or any Node.js backend. By @zizzamia #265 #283
- **feat**: exposed the `getName` and `getAvatar` utilities to assist in retrieving name and avatar identity information. These utilities come in handy when working with Next.js or any Node.js backend. By @zizzamia #265 #283 b795268

## 0.11.2

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<p align="center">
<a href="https://onchainkit.xyz">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./site/docs/public/logo/v0-11.png">
<img alt="OnchainKit logo vibes" src="./site/docs/public/logo/v0-11.png" width="auto">
<source media="(prefers-color-scheme: dark)" srcset="./site/docs/public/logo/v0-12.png">
<img alt="OnchainKit logo vibes" src="./site/docs/public/logo/v0-12.png" width="auto">
</picture>
</a>
</p>
Expand Down
4 changes: 4 additions & 0 deletions site/docs/pages/frame/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,13 @@ type FrameMetadataReact = FrameMetadataType & {

```ts
type FrameMetadataType = {
accepts?: {
[protocolIdentifier: string]: string;
}; // The minimum client protocol version accepted for the given protocol identifier.
buttons?: [FrameButtonMetadata, ...FrameButtonMetadata[]]; // A list of strings which are the label for the buttons in the frame (max 4 buttons).
image: string | FrameImageMetadata; // An image which must be smaller than 10MB and should have an aspect ratio of 1.91:1
input?: FrameInputMetadata; // The text input to use for the Frame.
isOpenFrame?: boolean; // A boolean indicating if the frame uses the Open Frames standard.
postUrl?: string; // A valid POST URL to send the Signature Packet to.
refreshPeriod?: number; // A period in seconds at which the app should expect the image to update.
state?: object; // A string containing serialized state (e.g. JSON) passed to the frame server.
Expand Down
Binary file added site/docs/public/logo/v0-12.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions src/frame/components/FrameMetadata.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,39 @@ describe('FrameMetadata', () => {
expect(meta.container.querySelector('meta[property="og:title"]')).toBeNull();
expect(meta.container.querySelectorAll('meta').length).toBe(3);
});

describe('when using isOpenFrame true', () => {
it('renders', () => {
const meta = render(<FrameMetadata isOpenFrame image="https://example.com/image.png" />);
expect(meta.container.querySelectorAll('meta').length).toBe(5);
});

it('renders with accepts', () => {
const meta = render(
<FrameMetadata
accepts={{ xmtp: '1.0.0' }}
isOpenFrame
image="https://example.com/image.png"
/>,
);
expect(
meta.container.querySelector('meta[property="of:accepts:xmtp"]')?.getAttribute('content'),
).toBe('1.0.0');
expect(meta.container.querySelectorAll('meta').length).toBe(6);
});

it('renders with image', () => {
const meta = render(
<FrameMetadata
accepts={{ xmtp: '1.0.0' }}
isOpenFrame
image="https://example.com/image.png"
/>,
);
expect(
meta.container.querySelector('meta[property="of:image"]')?.getAttribute('content'),
).toBe('https://example.com/image.png');
expect(meta.container.querySelectorAll('meta').length).toBe(6);
});
});
});
12 changes: 12 additions & 0 deletions src/frame/components/FrameMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ import type { FrameMetadataReact } from '../types';
* ```
*
* @param {FrameMetadataReact} props - The metadata for the frame.
* @param {{ [protocolIdentifier: string]: string; }} accepts - The types of protocol the frame accepts.
* @param {Array<{ label: string, action?: string }>} props.buttons - The buttons.
* @param {string | { src: string, aspectRatio?: string }} props.image - The image URL.
* @param {string} props.input - The input text.
* @param {boolean} props.isOpenFrame: Whether the frame uses the Open Frames standard.
* @param {string} props.ogDescription - The Open Graph description.
* @param {string} props.ogTitle - The Open Graph title.
* @param {string} props.postUrl - The post URL.
Expand All @@ -45,9 +47,11 @@ import type { FrameMetadataReact } from '../types';
* @returns {React.ReactElement} The FrameMetadata component.
*/
export function FrameMetadata({
accepts = {},
buttons,
image,
input,
isOpenFrame = false,
ogDescription,
ogTitle,
postUrl,
Expand Down Expand Up @@ -130,6 +134,14 @@ export function FrameMetadata({
{!!refreshPeriodToUse && (
<meta property="fc:frame:refresh_period" content={refreshPeriodToUse.toString()} />
)}

{!!isOpenFrame && <meta property="of:version" content="vNext" />}

{!!isOpenFrame && accepts && accepts['xmtp'] && (
<meta property={`of:accepts:xmtp`} content={accepts['xmtp']} />
)}

{!!isOpenFrame && imageSrc && <meta property="of:image" content={imageSrc} />}
</Wrapper>
);
}
57 changes: 55 additions & 2 deletions src/frame/getFrameHtmlResponse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,59 @@ describe('getFrameHtmlResponse', () => {
);
expect(html).not.toContain('<script>alert("XSS")</script>');
});
});

export { getFrameHtmlResponse };
describe('when using isOpenFrame true', () => {
it('should return correct HTML with all parameters', () => {
const html = getFrameHtmlResponse({
accepts: { 'protocol-identifier': '1.0.0' },
buttons: [
{ label: 'button1', action: 'post' },
{ label: 'button2', action: 'mint', target: 'https://example.com' },
{ label: 'button3', action: 'post_redirect' },
{ label: 'button4' },
],
image: {
src: 'https://example.com/image.png',
aspectRatio: '1.91:1',
},
input: {
text: 'Enter a message...',
},
isOpenFrame: true,
postUrl: 'https://example.com/api/frame',
refreshPeriod: 10,
state: {
counter: 1,
},
});

expect(html).toBe(`<!DOCTYPE html>
<html>
<head>
<meta property="og:description" content="Frame description" />
<meta property="og:title" content="Frame title" />
<meta property="fc:frame" content="vNext" />
<meta property="fc:frame:button:1" content="button1" />
<meta property="fc:frame:button:1:action" content="post" />
<meta property="fc:frame:button:2" content="button2" />
<meta property="fc:frame:button:2:action" content="mint" />
<meta property="fc:frame:button:2:target" content="https://example.com" />
<meta property="fc:frame:button:3" content="button3" />
<meta property="fc:frame:button:3:action" content="post_redirect" />
<meta property="fc:frame:button:4" content="button4" />
<meta property="og:image" content="https://example.com/image.png" />
<meta property="fc:frame:image" content="https://example.com/image.png" />
<meta property="fc:frame:image:aspect_ratio" content="1.91:1" />
<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" />
<meta property="of:version" content="vNext" />
<meta property="of:accepts:protocol-identifier" content="1.0.0" />
<meta property="of:image" content="https://example.com/image.png" />
</head>
</html>`);
});
});
});
19 changes: 18 additions & 1 deletion src/frame/getFrameHtmlResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ type FrameMetadataHTMLResponse = FrameMetadataType & {
/**
* Returns an HTML string containing metadata for a new valid frame.
*
* @param accepts: The types of protocol the frame accepts.
* @param buttons: The buttons to use for the frame.
* @param image: The image to use for the frame.
* @param input: The text input to use for the frame.
* @param isOpenFrame: Whether the frame uses the Open Frames standard.
* @param ogDescription: The Open Graph description for the frame.
* @param ogTitle: The Open Graph title for the frame.
* @param postUrl: The URL to post the frame to.
Expand All @@ -19,9 +21,11 @@ type FrameMetadataHTMLResponse = FrameMetadataType & {
* @returns An HTML string containing metadata for the frame.
*/
function getFrameHtmlResponse({
accepts = {},
buttons,
image,
input,
isOpenFrame = false,
ogDescription,
ogTitle,
postUrl,
Expand Down Expand Up @@ -79,14 +83,27 @@ function getFrameHtmlResponse({
? ` <meta property="fc:frame:refresh_period" content="${refreshPeriodToUse.toString()}" />\n`
: '';

let ofHtml = '';
// Set the Open Frames metadata
if (isOpenFrame) {
ofHtml = ` <meta property="of:version" content="vNext" />\n`;
const ofAcceptsHtml = Object.keys(accepts)
.map((protocolIdentifier) => {
return ` <meta property="of:accepts:${protocolIdentifier}" content="${accepts[protocolIdentifier]}" />\n`;
})
.join('');
const ofImageHtml = ` <meta property="of:image" content="${imgSrc}" />\n`;
ofHtml += ofAcceptsHtml + ofImageHtml;
}

// Return the HTML string containing all the metadata.
let html = `<!DOCTYPE html>
<html>
<head>
<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}${stateHtml}
${buttonsHtml}${ogImageHtml}${imageHtml}${inputHtml}${postUrlHtml}${refreshPeriodHtml}${stateHtml}${ofHtml}
</head>
</html>`;

Expand Down
31 changes: 31 additions & 0 deletions src/frame/getFrameMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,35 @@ describe('getFrameMetadata', () => {
'%7B%22counter%22%3A1%2C%22xss%22%3A%22%3Cscript%3Ealert(%5C%22XSS%5C%22)%3C%2Fscript%3E%22%7D',
});
});

describe('when using isOpenFrame true', () => {
it('should return the correct metadata', () => {
expect(
getFrameMetadata({
accepts: { 'protocol-identifier': '1.0.0' },
buttons: [
{ label: 'button1', action: 'post' },
{ label: 'button2', action: 'post_redirect' },
{ label: 'button3' },
],
image: { src: 'image', aspectRatio: '1.91:1' },
isOpenFrame: true,
postUrl: 'post_url',
}),
).toEqual({
'fc:frame': 'vNext',
'fc:frame:button:1': 'button1',
'fc:frame:button:1:action': 'post',
'fc:frame:button:2': 'button2',
'fc:frame:button:2:action': 'post_redirect',
'fc:frame:button:3': 'button3',
'fc:frame:image': 'image',
'fc:frame:image:aspect_ratio': '1.91:1',
'fc:frame:post_url': 'post_url',
'of:version': 'vNext',
'of:accepts:protocol-identifier': '1.0.0',
'of:image': 'image',
});
});
});
});
19 changes: 17 additions & 2 deletions src/frame/getFrameMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import { FrameMetadataResponse, FrameMetadataType } from './types';

/**
* This function generates the metadata for a Farcaster Frame.
* @param accepts: The types of protocol the frame accepts.
* @param buttons: The buttons to use for the frame.
* @param image: The image to use for the frame.
* @param input: The text input to use for the frame.
* @param isOpenFrame: Whether the frame uses the Open Frames standard.
* @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 ({
accepts = {},
buttons,
image,
input,
isOpenFrame = false,
postUrl,
post_url,
refreshPeriod,
Expand All @@ -26,14 +30,16 @@ export const getFrameMetadata = function ({
const metadata: Record<string, string> = {
'fc:frame': 'vNext',
};
let imageSrc = '';
if (typeof image === 'string') {
metadata['fc:frame:image'] = image;
imageSrc = image;
} else {
metadata['fc:frame:image'] = image.src;
imageSrc = image.src;
if (image.aspectRatio) {
metadata['fc:frame:image:aspect_ratio'] = image.aspectRatio;
}
}
metadata['fc:frame:image'] = imageSrc;
if (input) {
metadata['fc:frame:input:text'] = input.text;
}
Expand All @@ -60,5 +66,14 @@ export const getFrameMetadata = function ({
if (state) {
metadata['fc:frame:state'] = encodeURIComponent(JSON.stringify(state));
}
if (isOpenFrame) {
metadata['of:version'] = 'vNext';
if (accepts) {
Object.keys(accepts).forEach((protocolIdentifier) => {
metadata[`of:accepts:${protocolIdentifier}`] = accepts[protocolIdentifier];
});
}
metadata['of:image'] = imageSrc;
}
return metadata;
};
4 changes: 4 additions & 0 deletions src/frame/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,13 @@ export type FrameMetadataReact = FrameMetadataType & {
* Note: exported as public Type
*/
export type FrameMetadataType = {
accepts?: {
[protocolIdentifier: string]: string;
}; // The minimum client protocol version accepted for the given protocol identifier.
buttons?: [FrameButtonMetadata, ...FrameButtonMetadata[]]; // A list of strings which are the label for the buttons in the frame (max 4 buttons).
image: string | FrameImageMetadata; // An image which must be smaller than 10MB and should have an aspect ratio of 1.91:1
input?: FrameInputMetadata; // The text input to use for the Frame.
isOpenFrame?: boolean; // A boolean indicating if the frame uses the Open Frames standard.
/** @deprecated Prefer `postUrl` */
post_url?: string;
postUrl?: string; // A valid POST URL to send the Signature Packet to.
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = '0.11.3';
export const version = '0.12.0';

0 comments on commit b8aa317

Please sign in to comment.