Skip to content

Commit

Permalink
Merge pull request #436 from xmtp-labs/nm/upgrade-frames-client
Browse files Browse the repository at this point in the history
Upgrade Frames Client
  • Loading branch information
neekolas authored Feb 16, 2024
2 parents cc8962c + 0e35641 commit 96a6888
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 44 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@xmtp/content-type-remote-attachment": "^1.1.4",
"@xmtp/content-type-reply": "^1.1.5",
"@xmtp/experimental-content-type-screen-effect": "^1.0.2",
"@xmtp/frames-client": "0.2.0",
"@xmtp/frames-client": "0.3.2",
"@xmtp/react-sdk": "^5.0.1",
"buffer": "^6.0.3",
"date-fns": "^2.29.3",
Expand Down
14 changes: 9 additions & 5 deletions src/component-library/components/Frame/Frame.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { FrameButton } from "../../../helpers/getFrameInfo";
import { GhostButton } from "../GhostButton/GhostButton";

type FrameProps = {
image: string;
title: string;
buttons: string[];
handleClick: (buttonNumber: number) => Promise<void>;
buttons: FrameButton[];
handleClick: (
buttonNumber: number,
action: FrameButton["action"],
) => Promise<void>;
frameButtonUpdating: number;
};

Expand All @@ -23,12 +27,12 @@ export const Frame = ({
return null;
}
const handlePress = () => {
void handleClick(index + 1);
void handleClick(index + 1, button.action);
};
return (
<GhostButton
key={button}
label={button}
key={button.text}
label={button.text}
onClick={handlePress}
isLoading={frameButtonUpdating === index + 1}
isDisabled={frameButtonUpdating > 0}
Expand Down
58 changes: 29 additions & 29 deletions src/controllers/FullMessageController.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { CachedConversation, CachedMessageWithId } from "@xmtp/react-sdk";
import { Client, useClient } from "@xmtp/react-sdk";
import { useClient } from "@xmtp/react-sdk";
import { FramesClient } from "@xmtp/frames-client";
import { useEffect, useState } from "react";
import type { PrivateKeyAccount, Transport, WalletClient } from "viem";
import type { mainnet } from "wagmi";
import { useWalletClient } from "wagmi";
import { FullMessage } from "../component-library/components/FullMessage/FullMessage";
import { classNames, shortAddress } from "../helpers";
import MessageContentController from "./MessageContentController";
import { useXmtpStore } from "../store/xmtp";
import { Frame } from "../component-library/components/Frame/Frame";
import type { FrameButton } from "../helpers/getFrameInfo";
import { getFrameInfo } from "../helpers/getFrameInfo";
import { readMetadata } from "../helpers/openFrames";

interface FullMessageControllerProps {
message: CachedMessageWithId;
Expand All @@ -21,7 +20,7 @@ interface FullMessageControllerProps {
export type FrameInfo = {
image: string;
title: string;
buttons: string[];
buttons: FrameButton[];
postUrl: string;
};

Expand All @@ -31,47 +30,48 @@ export const FullMessageController = ({
isReply,
}: FullMessageControllerProps) => {
const { client } = useClient();
const { data: walletClient } = useWalletClient();

const conversationTopic = useXmtpStore((state) => state.conversationTopic);

const [frameInfo, setFrameInfo] = useState<FrameInfo | undefined>(undefined);
const [frameButtonUpdating, setFrameButtonUpdating] = useState<number>(0);

const handleFrameButtonClick = async (buttonIndex: number) => {
if (!frameInfo) {
const handleFrameButtonClick = async (
buttonIndex: number,
action: FrameButton["action"],
) => {
if (!frameInfo || !client) {
return;
}
const frameUrl = frameInfo.image;
const button = frameInfo.buttons[buttonIndex];

setFrameButtonUpdating(buttonIndex);

const xmtpClient = await Client.create(
walletClient as WalletClient<
Transport,
typeof mainnet,
PrivateKeyAccount
>,
);
const framesClient = new FramesClient(xmtpClient);
const framesClient = new FramesClient(client);

const payload = await framesClient.signFrameAction({
frameUrl,
buttonIndex,
conversationTopic: conversationTopic as string,
participantAccountAddresses: [
client?.address as string,
conversation.peerAddress,
],
participantAccountAddresses: [client.address, conversation.peerAddress],
});

const updatedFrameMetadata = await FramesClient.postToFrame(
frameInfo.postUrl,
payload,
);
const updatedFrameInfo = getFrameInfo(updatedFrameMetadata.extractedTags);

setFrameInfo(updatedFrameInfo);
if (action === "post" || !action) {
const updatedFrameMetadata = await framesClient.proxy.post(
button?.target || frameInfo.postUrl,
payload,
);
const updatedFrameInfo = getFrameInfo(updatedFrameMetadata.extractedTags);
setFrameInfo(updatedFrameInfo);
} else if (action === "post_redirect") {
const { redirectedTo } = await framesClient.proxy.postRedirect(
button?.target || frameInfo.postUrl,
payload,
);
window.open(redirectedTo, "_blank");
} else if (action === "link" && button?.target) {
window.open(button.target, "_blank");
}
setFrameButtonUpdating(0);
};

Expand All @@ -86,7 +86,7 @@ export const FullMessageController = ({
const isUrl = !!word.match(urlRegex)?.[0];

if (isUrl) {
const metadata = await FramesClient.readMetadata(word);
const metadata = await readMetadata(word);
if (metadata) {
const info = getFrameInfo(metadata.extractedTags);
setFrameInfo(info);
Expand Down
45 changes: 40 additions & 5 deletions src/helpers/getFrameInfo.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,56 @@
import { getButtonIndex, mediaUrl } from "./openFrames";

const BUTTON_PREFIX = "fc:frame:button:";
const IMAGE_PREFIX = "fc:frame:image";
const POST_URL_PREFIX = "fc:frame:post_url";
const TITLE_PREFIX = "og:title";

const ALLOWED_ACTIONS = ["post", "post_redirect", "link", "mint"] as const;

export type FrameButton = {
text?: string;
action?: (typeof ALLOWED_ACTIONS)[number];
target?: string;
};

function addButtonTag(
existingButtons: FrameButton[],
key: string,
value: string,
): FrameButton[] {
const buttons = existingButtons;
const buttonIndex = getButtonIndex(key);
if (buttonIndex) {
buttons[buttonIndex] = buttons[buttonIndex] || {};
if (
key.endsWith(":action") &&
ALLOWED_ACTIONS.includes(value as (typeof ALLOWED_ACTIONS)[number])
) {
buttons[buttonIndex].action = value as (typeof ALLOWED_ACTIONS)[number];
} else if (key.endsWith(":target")) {
buttons[buttonIndex].target = value;
} else if (key.endsWith(buttonIndex.toString())) {
buttons[buttonIndex].text = value;
}
}
return buttons;
}

export function getFrameInfo(extractedTags: Record<string, string>) {
const buttons: string[] = [];
let buttons: FrameButton[] = [];
let image = "";
let postUrl = "";
let title = "";
for (const key in extractedTags) {
if (key) {
if (key.startsWith(BUTTON_PREFIX)) {
const buttonIndex = parseInt(key.replace(BUTTON_PREFIX, ""), 10);
buttons[buttonIndex] = extractedTags[key];
buttons = addButtonTag(buttons, key, extractedTags[key]);
}
if (key.startsWith(IMAGE_PREFIX)) {
image = extractedTags[key];
const imageUrl = extractedTags[key];
image = imageUrl.startsWith("data:")
? extractedTags[key]
: mediaUrl(imageUrl);
}
if (key.startsWith(POST_URL_PREFIX)) {
postUrl = extractedTags[key];
Expand All @@ -26,7 +61,7 @@ export function getFrameInfo(extractedTags: Record<string, string>) {
}
}
return {
buttons,
buttons: buttons.filter((b) => b),
image,
postUrl,
title,
Expand Down
17 changes: 17 additions & 0 deletions src/helpers/openFrames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { OpenFramesProxy, type FramesApiResponse } from "@xmtp/frames-client";

const proxy = new OpenFramesProxy();
const BUTTON_INDEX_REGEX = /fc:frame:button:(\d)(?:$|:).*/;

export const readMetadata = async (url: string): Promise<FramesApiResponse> =>
proxy.readMetadata(url);

export const mediaUrl = (url: string): string => proxy.mediaUrl(url);

export const getButtonIndex = (property: string): number | undefined => {
const matches = property.match(BUTTON_INDEX_REGEX);
if (matches?.length === 2) {
return parseInt(matches[1], 10);
}
return undefined;
};

0 comments on commit 96a6888

Please sign in to comment.