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

feat: init Open Frame spec support #285

Merged
merged 6 commits into from
Apr 12, 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/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?: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

love this approach! 👏

[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';
Loading