From 2e9587e6067797fe20deb18b5a798f55b411f644 Mon Sep 17 00:00:00 2001 From: fabri Date: Mon, 16 Dec 2024 13:42:36 -0300 Subject: [PATCH 01/31] fdeeplink framekit --- packages/docs/pages/plugins/framekit.mdx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/docs/pages/plugins/framekit.mdx b/packages/docs/pages/plugins/framekit.mdx index e4b38dd7f..c70f2fb72 100644 --- a/packages/docs/pages/plugins/framekit.mdx +++ b/packages/docs/pages/plugins/framekit.mdx @@ -45,7 +45,7 @@ Properties: - `url`: URL of the transaction receipt scanner like basescan, etherscan, etc. -## Dm and Groups on Converse +## Converse You can send messages to a user or group on Converse using the `sendConverseDmFrame` and `sendConverseGroupFrame` methods. @@ -57,6 +57,17 @@ await framekit.sendConverseDmFrame("userAddress", "Hello, how are you?"); await framekit.sendConverseGroupFrame("groupId", "gm all!"); ``` +## Coinbase + +Coinbase does not render frames but you can deeplink into other users inside the wallet. + +```typescript +// Send a message to a user with an optional pretext +await framekit.send( + "https://go.cb-w.com/messaging?address=0x93E2fc3e99dFb1238eB9e0eF2580EFC5809C7204", +); +``` + ## Custom Custom frames allow you to create interactive UI elements. Here's how to create a token price frame: From c2f14745dfd28bc22770c6ebad0363b094688c33 Mon Sep 17 00:00:00 2001 From: fabri Date: Mon, 16 Dec 2024 13:46:32 -0300 Subject: [PATCH 02/31] deeplinks --- packages/docs/pages/plugins/framekit.mdx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/docs/pages/plugins/framekit.mdx b/packages/docs/pages/plugins/framekit.mdx index c70f2fb72..3c1bb6be7 100644 --- a/packages/docs/pages/plugins/framekit.mdx +++ b/packages/docs/pages/plugins/framekit.mdx @@ -50,11 +50,17 @@ Properties: You can send messages to a user or group on Converse using the `sendConverseDmFrame` and `sendConverseGroupFrame` methods. ```typescript +// Send a message to a user +await framekit.sendConverseDmFrame(userAddress); + // Send a message to a user with an optional pretext -await framekit.sendConverseDmFrame("userAddress", "Hello, how are you?"); +await framekit.sendConverseDmFrame(userAddress, "Hello, how are you?"); + +// Send a message to a group +await framekit.sendConverseGroupFrame(groupId); // Send a message to a group with an optional pretext -await framekit.sendConverseGroupFrame("groupId", "gm all!"); +await framekit.sendConverseGroupFrame(groupId, "gm all!"); ``` ## Coinbase @@ -63,9 +69,7 @@ Coinbase does not render frames but you can deeplink into other users inside the ```typescript // Send a message to a user with an optional pretext -await framekit.send( - "https://go.cb-w.com/messaging?address=0x93E2fc3e99dFb1238eB9e0eF2580EFC5809C7204", -); +await framekit.send("https://go.cb-w.com/messaging?address=" + userAddress); ``` ## Custom From c3c812d616156cdb1877ae77bfff8379f7811e97 Mon Sep 17 00:00:00 2001 From: fabri Date: Mon, 16 Dec 2024 13:47:09 -0300 Subject: [PATCH 03/31] deeplinks --- packages/docs/pages/plugins/framekit.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/docs/pages/plugins/framekit.mdx b/packages/docs/pages/plugins/framekit.mdx index 3c1bb6be7..8e003d831 100644 --- a/packages/docs/pages/plugins/framekit.mdx +++ b/packages/docs/pages/plugins/framekit.mdx @@ -65,11 +65,13 @@ await framekit.sendConverseGroupFrame(groupId, "gm all!"); ## Coinbase +:::warning Coinbase does not render frames but you can deeplink into other users inside the wallet. +::: ```typescript // Send a message to a user with an optional pretext -await framekit.send("https://go.cb-w.com/messaging?address=" + userAddress); +await context.send("https://go.cb-w.com/messaging?address=" + userAddress); ``` ## Custom From ada448a35f28da5e185da8f17ec7df723a73dbb7 Mon Sep 17 00:00:00 2001 From: fabri Date: Mon, 16 Dec 2024 14:57:03 -0300 Subject: [PATCH 04/31] circle --- packages/docs/pages/plugins/circle.mdx | 42 ++++++++++++++++++++++++ packages/docs/pages/plugins/resolver.mdx | 1 + templates/ens/src/skills/pay.ts | 15 ++------- templates/playground/src/skills/pay.ts | 6 +++- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/packages/docs/pages/plugins/circle.mdx b/packages/docs/pages/plugins/circle.mdx index de3c6250f..5da50c969 100644 --- a/packages/docs/pages/plugins/circle.mdx +++ b/packages/docs/pages/plugins/circle.mdx @@ -2,6 +2,48 @@ The `WalletService` class in MessageKit provides a way to create agent wallets that can perform gasless payments and transfers on Base. +- Gasless +- Onramp +- Offramp +- Swaps +- Transfers + +## Wallet Management + +The `getWallet` method will retrieve an existing wallet or create a new one if it doesn't exist: + +```tsx +// Get the wallet service +const { walletService } = context; + +// Get or create a wallet +const wallet = await walletService.getWallet(identifier or address); + +// Explicitly create a new wallet +const isCreated = await walletService.createWallet(identifier or address); + +// Delete a wallet +await walletService.deleteWallet(identifier or address); +``` + +## Actions + +Perform actions on the Agent Wallet like transfers and swaps or checking the balance. + +```tsx +// Get the wallet service +const { walletService } = context; + +// Check balance +const { address, balance } = await walletService.checkBalance(identifier or address); + +// Transfer between wallets +await walletService.transfer(identifier or address, identifier or address, amount); + +// Swap assets (USDC and ETH) +await walletService.swap(identifier or address, fromAssetId, toAssetId, amount); +``` + ## Requirements Visit the [Circle Developer Portal](https://developer.circle.com/) to create an API key and entity secret. diff --git a/packages/docs/pages/plugins/resolver.mdx b/packages/docs/pages/plugins/resolver.mdx index 2523f5088..cf47b266e 100644 --- a/packages/docs/pages/plugins/resolver.mdx +++ b/packages/docs/pages/plugins/resolver.mdx @@ -33,6 +33,7 @@ The resolver returns a `UserInfo` object containing: The resolver implements automatic caching to improve performance and reduce API calls. You can manage the cache using: ```typescript +// Clear cache for specific address context.clearCache(address); // or import directly diff --git a/templates/ens/src/skills/pay.ts b/templates/ens/src/skills/pay.ts index f2d837e26..765c854b7 100644 --- a/templates/ens/src/skills/pay.ts +++ b/templates/ens/src/skills/pay.ts @@ -24,10 +24,6 @@ export const pay: Skill[] = [ default: "", type: "username", }, - address: { - default: "", - type: "address", - }, }, }, { @@ -48,18 +44,11 @@ export async function handler(context: Context) { message: { content: { skill, - params: { amount, token, username, address }, + params: { amount, token, username }, }, }, } = context; - let receiverAddress = address; - if (username) { - receiverAddress = (await getUserInfo(username))?.address; - } - if (address) { - //Prioritize address over username - receiverAddress = address; - } + let receiverAddress = username?.address; if (skill === "tip") { let tipAmount = 1; await context.framekit.requestPayment(receiverAddress, tipAmount); diff --git a/templates/playground/src/skills/pay.ts b/templates/playground/src/skills/pay.ts index f07365303..8b965e6a2 100644 --- a/templates/playground/src/skills/pay.ts +++ b/templates/playground/src/skills/pay.ts @@ -31,5 +31,9 @@ export async function handler(context: Context) { const { amount: amountSend, token: tokenSend, username } = params; - await context.framekit.requestPayment(username, amountSend, tokenSend); + await context.framekit.requestPayment( + username?.address, + amountSend, + tokenSend, + ); } From 11a7192f0c44fe451667b1c28d3f6799fe0769f2 Mon Sep 17 00:00:00 2001 From: fabri Date: Mon, 16 Dec 2024 15:10:42 -0300 Subject: [PATCH 05/31] username how to --- packages/message-kit/src/plugins/framekit.ts | 6 +++--- templates/ens/example_prompt.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/message-kit/src/plugins/framekit.ts b/packages/message-kit/src/plugins/framekit.ts index 8484a4acd..3438ddc6f 100644 --- a/packages/message-kit/src/plugins/framekit.ts +++ b/packages/message-kit/src/plugins/framekit.ts @@ -8,9 +8,9 @@ export interface Frame { } const framesUrl = - process.env.NODE_ENV === "production" - ? "https://frames.message-kit.org" - : "https://frames.ngrok.app"; + process.env.FRAME_URL !== undefined + ? process.env.FRAME_URL + : "https://frames.message-kit.org"; export class FrameKit { private context: Context; diff --git a/templates/ens/example_prompt.md b/templates/ens/example_prompt.md index ec450ab64..a513a60d1 100644 --- a/templates/ens/example_prompt.md +++ b/templates/ens/example_prompt.md @@ -13,14 +13,14 @@ You are a helpful agent called @bot that lives inside a web3 messaging app calle - Do not make guesses or assumptions - Only answer if the verified information is in the prompt. - Focus only on helping users with operations detailed below. -- Date: Sun, 15 Dec 2024 18:09:17 GMT, +- Date: Mon, 16 Dec 2024 18:02:47 GMT, ## User context - Start by fetch their domain from or Converse username - Call the user by their name or domain, in case they have one - Ask for a name (if they don't have one) so you can suggest domains. -- Message sent date: 2024-12-15T18:09:34.050Z +- Message sent date: 2024-12-16T18:09:00.565Z - Users address is: 0x40f08f0f853d1c42c61815652b7ccd5a50f0be09 - Users name is: ArizonaOregon - Converse username is: ArizonaOregon @@ -31,7 +31,7 @@ You are a helpful agent called @bot that lives inside a web3 messaging app calle /info [domain] - Get detailed information about an ENS domain including owner, expiry date, and resolver. /register [domain] - Register a new ENS domain. Returns a URL to complete the registration process. /renew [domain] - Extend the registration period of your ENS domain. Returns a URL to complete the renewal. -/pay [amount] [token] [username] [address] - Send a specified amount of a cryptocurrency to a destination address. +/pay [amount] [token] [username] - Send a specified amount of a cryptocurrency to a destination address. When tipping, you can asume its 1 usdc. /tip [username] - Send 1 usdc. From a971664db88754d8ba8570e275196d068066d704 Mon Sep 17 00:00:00 2001 From: fabri Date: Mon, 16 Dec 2024 19:10:02 -0300 Subject: [PATCH 06/31] framekit --- .cursorrules | 209 ----------------- package.json | 2 +- packages/create-message-kit/package.json | 2 +- packages/docs/pages/plugins/framekit.mdx | 42 +++- packages/docs/pages/skills/concierge.mdx | 3 +- .../framekit/src/app/dm/[address]/page.tsx | 60 +++-- packages/framekit/src/app/utils/resolver.ts | 215 +++++++++++++++--- packages/framekit/src/components/Chat.tsx | 49 ++-- packages/message-kit/package.json | 2 +- packages/message-kit/src/index.ts | 2 +- packages/message-kit/src/lib/core.ts | 4 - packages/message-kit/src/plugins/framekit.ts | 49 ++-- packages/message-kit/src/plugins/resolver.ts | 11 - packages/message-kit/src/skills/concierge.ts | 31 ++- templates/ens/src/skills/pay.ts | 8 +- templates/paymentagent/example_prompt.md | 11 +- templates/playground/src/skills/cash.ts | 8 +- templates/playground/src/skills/drip.ts | 5 +- templates/playground/src/skills/pay.ts | 5 +- templates/playground/src/skills/token.ts | 4 +- 20 files changed, 369 insertions(+), 353 deletions(-) diff --git a/.cursorrules b/.cursorrules index 115b460ee..e69de29bb 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,209 +0,0 @@ -# MessageKit - -# Skill Examples - -### Check if a Domain is Available - - -import { ensUrl } from "../index.js"; -import { Context } from "@xmtp/message-kit"; -import type { Skill } from "@xmtp/message-kit"; - -// Define Skill -export const checkDomain: Skill[] = [ - { - skill: "check", - handler: handler, - examples: ["/check vitalik.eth", "/check fabri.base.eth"], - description: "Check if a domain is available.", - params: { - domain: { - type: "string", - }, - }, - }, -]; - -// Handler Implementation -export async function handler(context: Context) { - const { - message: { - content: { - params: { domain }, - }, - }, - } = context; - - const data = await getUserInfo(domain); - - if (!data?.address) { - let message = `Looks like ${domain} is available! Here you can register it: ${ensUrl}${domain} or would you like to see some cool alternatives?`; - return { - code: 200, - message, - }; - } else { - let message = `Looks like ${domain} is already registered!`; - await context.executeSkill("/cool " + domain); - return { - code: 404, - message, - }; - } -} - -### Generate a payment request - - -import { Context } from "@xmtp/message-kit"; -import type { Skill } from "@xmtp/message-kit"; - -// Define Skill -export const paymentRequest: Skill[] = [ - { - skill: "pay", - examples: [ - "/pay 10 vitalik.eth", - "/pay 1 usdc to 0xC60E6Bb79322392761BFe3081E302aEB79B30B03", - ], - description: - "Send a specified amount of a cryptocurrency to a destination address. \nWhen tipping, you can assume it's 1 USDC.", - handler: handler, - params: { - amount: { - default: 10, - type: "number", - }, - token: { - default: "usdc", - type: "string", - values: ["eth", "dai", "usdc", "degen"], // Accepted tokens - }, - username: { - default: "", - type: "username", - }, - address: { - default: "", - type: "address", - }, - }, - }, -]; - -// Handler Implementation -export async function handler(context: Context) { - const { - message: { - content: { - params: { amount, token, username, address }, - }, - }, - } = context; - let receiverAddress = address; - if (username) { - receiverAddress = (await getUserInfo(username))?.address; - } - if (address) { - // Prioritize address over username - receiverAddress = address; - } - - await context.framekit.requestPayment(receiverAddress, amount, token); -} - - -# Docs - -# Structure - -## File structure - -Each app consists of the following files: - -``` -agent/ -├── src/ -│ └── index.ts -│ └── prompt.ts # Optional -│ └── plugins/ # Optional -│ └── ... -│ └── skills/ # Optional -│ └── ... -│ └── vibes/ # Optional -│ └── ... -├── tsconfig.json -├── package.json -└── .env -``` - -## Agent - -This is the main function that runs the listener. - -```jsx -import { Agent, run, Context } from "@xmtp/message-kit"; - -const agent: Agent = { - name: "Agent Name", - tag: "@bot", - description: "Agent Description", - skills: [skill1, skill2], - onMessage: async (context: Context) => { - /* Logs every message in a conversation. - If not declared, the agent will automatically use the defined skills. - Alternatively, you can implement your own logic here. */ - }, - config: { - // Optional parameters - }, -}; -//starts the agent -run(agent); -``` - -#### Config parameters - -- `privateKey`: the private key of the agent wallet, like any normal wallet private key. -- `experimental`: experimental features like logging all group messages. Default is `false`. -- `attachments`: to receive attachments. Default is `false`. -- `gptModel`: model to be used. Default is `gpt-4o`. -- `client`: Optional parameters to pass to the XMTP client. -- `agent`: Custom agent to be used. Default is to create the skills in the `src/skills.ts` file. -- `hideInitLogMessage`: hide the init log message with messagekit logo and stuff -- `memberChange`: if true, member changes will be enabled, like adding members to the group - -## Skills - -Skills are the actions of the agent. They are defined in the `src/skills/your-skill.ts` file. - -```tsx -import { Skill } from "@xmtp/message-kit"; - -export const checkDomain: Skill[] = [ - { - skill: // name of the skill - handler: // function to handle the skill - examples: // examples of the skill - description: // description of the skill - params: // params of the skill - }, -]; -``` - -## Vibes - -Vibes are the personalities of the agent. They are defined in the `src/vibes/your-vibe.ts` file. - -```tsx -import { Vibe } from "@xmtp/message-kit"; - -export const chill: Vibe = { - vibe: // name of the vibe - description: // description of the vibe - tone: // tone of the vibe - style: // style of the vibe -}; -``` - -> See [Vibes](/community/vibes) for more information. diff --git a/package.json b/package.json index 6fb1d73ca..4b647bbd1 100644 --- a/package.json +++ b/package.json @@ -56,4 +56,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} diff --git a/packages/create-message-kit/package.json b/packages/create-message-kit/package.json index ae7949ae5..6519e9169 100644 --- a/packages/create-message-kit/package.json +++ b/packages/create-message-kit/package.json @@ -36,4 +36,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} \ No newline at end of file +} diff --git a/packages/docs/pages/plugins/framekit.mdx b/packages/docs/pages/plugins/framekit.mdx index 8e003d831..0f1189ff9 100644 --- a/packages/docs/pages/plugins/framekit.mdx +++ b/packages/docs/pages/plugins/framekit.mdx @@ -13,7 +13,10 @@ You can request payments using the payment frame: ```typescript // Request 1 USDC payment to a specific address -await framekit.requestPayment(recipientAddress, 1, "USDC"); +const url = await FrameKit.requestPayment(recipientAddress, 1, "USDC"); + +// Send the url to the user +await context.send(url); ``` ## Wallet details @@ -22,7 +25,12 @@ You can send agent wallet info using the `sendWallet` method: ```typescript // Send agent wallet info -await framekit.sendWallet("0x93E2fc3e99dFb1238eB9e0eF2580EFC5809C7204"); +const url = await FrameKit.sendWallet( + "0x93E2fc3e99dFb1238eB9e0eF2580EFC5809C7204", +); + +// Send the url to the user +await context.send(url); ``` Properties: @@ -37,8 +45,10 @@ You can request receipts using the receipt frame: ```typescript // Request a receipt -await framekit.sendReceipt(urlOfTransaction); -// ie https://sepolia.basescan.org/tx/0x1234567890abcdef +const url = await FrameKit.sendReceipt(urlOfTransaction); + +// Send the url to the user +await context.send(url); ``` Properties: @@ -51,16 +61,25 @@ You can send messages to a user or group on Converse using the `sendConverseDmFr ```typescript // Send a message to a user -await framekit.sendConverseDmFrame(userAddress); +const url = await FrameKit.converseLink(userAddress); + +// Send the url to the user +await context.send(url); // Send a message to a user with an optional pretext -await framekit.sendConverseDmFrame(userAddress, "Hello, how are you?"); +const url = await FrameKit.converseLink(userAddress, "Hello, how are you?"); + +// Send the url to the user +await context.send(url); // Send a message to a group -await framekit.sendConverseGroupFrame(groupId); +const url = await FrameKit.converseGroup(groupId); // Send a message to a group with an optional pretext -await framekit.sendConverseGroupFrame(groupId, "gm all!"); +const url = await FrameKit.converseGroup(groupId, "gm all!"); + +// Send the url to the user +await context.send(url); ``` ## Coinbase @@ -71,7 +90,7 @@ Coinbase does not render frames but you can deeplink into other users inside the ```typescript // Send a message to a user with an optional pretext -await context.send("https://go.cb-w.com/messaging?address=" + userAddress); +await context.coinbaseLink(userAddress); ``` ## Custom @@ -96,7 +115,10 @@ const frame = { image: "https://example.com/weather.png", }; -await framekit.sendCustomFrame(frame); +const url = await FrameKit.sendCustomFrame(frame); + +// Send the url to the user +await context.send(url); ``` Properties: diff --git a/packages/docs/pages/skills/concierge.mdx b/packages/docs/pages/skills/concierge.mdx index d73fa6cca..c1d5a36bb 100644 --- a/packages/docs/pages/skills/concierge.mdx +++ b/packages/docs/pages/skills/concierge.mdx @@ -50,12 +50,13 @@ async function fund(context: Context, amount: number) { amount, walletData.agent_address, ); - await context.framekit.requestPayment( + const url = await FrameKit.requestPayment( walletData.agent_address, amount, "USDC", onRamp ? onRampURL : undefined, ); + await context.send(url); } ``` diff --git a/packages/framekit/src/app/dm/[address]/page.tsx b/packages/framekit/src/app/dm/[address]/page.tsx index 34871d2e9..55e51369e 100644 --- a/packages/framekit/src/app/dm/[address]/page.tsx +++ b/packages/framekit/src/app/dm/[address]/page.tsx @@ -1,8 +1,9 @@ "use client"; -import { Suspense, useEffect, useState } from "react"; +import { Suspense, useEffect, useState, useMemo } from "react"; import dynamic from "next/dynamic"; import { useParams } from "next/navigation"; import sdk, { type FrameContext } from "@farcaster/frame-sdk"; +import { getUserInfo, UserInfo } from "@/app/utils/resolver"; const Chat = dynamic(() => import("../../../components/Chat"), { ssr: false, @@ -10,12 +11,40 @@ const Chat = dynamic(() => import("../../../components/Chat"), { export default function ChatFrame(): JSX.Element { const params = useParams(); - const address = params.address as string; + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchUserInfo = async () => { + try { + console.log("Fetching user info for address:", params?.address); + const userInfo = await getUserInfo(params?.address as string); + console.log("Fetched user info:", userInfo); + setUser(userInfo ?? null); + } catch (error) { + console.error("Error fetching user info:", error); + } finally { + setLoading(false); + } + }; + + fetchUserInfo(); + }, [params?.address]); + + const memoizedUser = useMemo(() => user, [user]) as UserInfo; + + if (loading) { + return
Loading...
; + } + + if (!user) { + return
User not found
; + } return ( - + Loading...}> - + ); @@ -24,13 +53,13 @@ export default function ChatFrame(): JSX.Element { // Create a wrapper component that will render the full HTML function FrameHTML({ children, - address, + user, }: { children: React.ReactNode; - address: string; + user: UserInfo; }) { const baseUrl = `${process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"}`; - const image = `${baseUrl}/api/dm?address=${address}`; + const image = `${baseUrl}/api/dm?address=${user.address}`; console.log(image); return ( @@ -51,27 +80,26 @@ function FrameHTML({ ); } -function ChatContent(): JSX.Element { +function ChatContent({ user }: { user: UserInfo }): JSX.Element { const [isSDKLoaded, setIsSDKLoaded] = useState(false); const [context, setContext] = useState(); const params = useParams(); - const address = params.address as string; useEffect(() => { const initFrame = async () => { - setContext(await sdk.context); - sdk.actions.ready(); + if (!isSDKLoaded) { + setContext(await sdk.context); + sdk.actions.ready(); + setIsSDKLoaded(true); + } }; - if (sdk && !isSDKLoaded) { - setIsSDKLoaded(true); - initFrame(); - } + initFrame(); }, [isSDKLoaded]); return (
- +
); } diff --git a/packages/framekit/src/app/utils/resolver.ts b/packages/framekit/src/app/utils/resolver.ts index e56f42496..0d938c1e5 100644 --- a/packages/framekit/src/app/utils/resolver.ts +++ b/packages/framekit/src/app/utils/resolver.ts @@ -1,5 +1,4 @@ import { isAddress } from "viem"; - export const converseEndpointURL = "https://converse.xyz/profile/"; export type InfoCache = Map; @@ -11,38 +10,198 @@ export type ConverseProfile = { name: string | undefined; }; export type UserInfo = { + ensDomain?: string | undefined; + address?: string | undefined; + preferredName: string | undefined; + converseUsername?: string | undefined; + ensInfo?: EnsData | undefined; + avatar?: string | undefined; + converseEndpoint?: string | undefined; +}; + +export interface EnsData { address?: string; - preferredName?: string; avatar?: string; - bio?: string; -}; + avatar_small?: string; + converse?: string; + avatar_url?: string; + contentHash?: string; + description?: string; + ens?: string; + ens_primary?: string; + github?: string; + resolverAddress?: string; + twitter?: string; + url?: string; + wallets?: { + eth?: string; + }; +} -export async function getUserInfo(identifier: string): Promise { - try { - // Add CORS proxy or use environment variable for the API endpoint - const response = await fetch(`/api/resolve?identifier=${identifier}`, { - headers: { - Accept: "application/json", - }, - }); +class UserInfoCache { + private static instance: UserInfoCache; + private cache: InfoCache = new Map(); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + private constructor() {} + + public static getInstance(): UserInfoCache { + if (!UserInfoCache.instance) { + UserInfoCache.instance = new UserInfoCache(); } + return UserInfoCache.instance; + } - const data = await response.json(); - return { - address: data.address, - preferredName: data.preferredName || identifier, - avatar: data.avatar, - bio: data.bio, - }; - } catch (error) { - console.error("Error fetching user info:", error); - // Return basic info if resolution fails - return { - address: identifier, - preferredName: identifier, - }; + get(key: string): UserInfo | undefined { + return this.cache.get(key.toLowerCase()); + } + + set(key: string, data: UserInfo): void { + this.cache.set(key.toLowerCase(), data); + } + + clear(key?: string): void { + if (key) { + this.cache.delete(key.toLowerCase()); + } else { + this.cache.clear(); + } } } + +// Use the singleton instance +export const userInfoCache = UserInfoCache.getInstance(); + +export const getUserInfo = async ( + key: string, + clientAddress?: string, +): Promise => { + let data: UserInfo = { + ensDomain: undefined, + address: undefined, + converseUsername: undefined, + ensInfo: undefined, + avatar: undefined, + converseEndpoint: undefined, + preferredName: undefined, + }; + + if (typeof key !== "string") { + console.error("userinfo key must be a string"); + return data; + } + + const cachedData = userInfoCache.get(key); + if (cachedData) return cachedData; + + key = key?.toLowerCase(); + clientAddress = clientAddress?.toLowerCase(); + + // Determine user information based on provided key + if (isAddress(clientAddress || "")) { + data.address = clientAddress; + } else if (isAddress(key || "")) { + data.address = key; + } else if (key.includes(".eth")) { + data.ensDomain = key; + } else if (["@user", "@me", "@bot"].includes(key)) { + data.address = clientAddress; + data.ensDomain = key.replace("@", "") + ".eth"; + data.converseUsername = key.replace("@", ""); + } else if (key === "@alix") { + data.address = "0x3a044b218BaE80E5b9E16609443A192129A67BeA".toLowerCase(); + data.converseUsername = "alix"; + } else if (key === "@bo") { + data.address = "0xbc3246461ab5e1682baE48fa95172CDf0689201a".toLowerCase(); + data.converseUsername = "bo"; + } else { + data.converseUsername = key; + } + + data.preferredName = data.ensDomain || data.converseUsername || "Friend"; + const keyToUse = + data.address?.toLowerCase() || data.ensDomain || data.converseUsername; + + if (!keyToUse) { + console.log("Unable to determine a valid key for fetching user info."); + return data; + } else { + // Fetch ENS data + try { + const response = await fetch(`https://ensdata.net/${keyToUse}`); + if (response.status !== 200) { + if (process.env.MSG_LOG === "true") + console.log("- ENS data request failed for", keyToUse); + } else { + const ensData = (await response.json()) as EnsData; + if (ensData) { + data.ensInfo = ensData; + data.ensDomain = ensData.ens || data.ensDomain; + data.address = + ensData.address?.toLowerCase() || data.address?.toLowerCase(); + data.avatar = ensData.avatar_url || data.avatar; + } + } + } catch (error) { + console.error(`Failed to fetch ENS data for ${keyToUse}`); + } + //Converse profile + try { + const username = keyToUse.replace("@", ""); + const converseEndpoint = `${converseEndpointURL}${username}`; + const response = await fetchWithTimeout( + converseEndpoint, + { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ peer: username }), + }, + 5000, + ); + if (!response?.ok) { + console.error( + `Converse profile request failed with status ${response?.status}`, + ); + } + const converseData = (await response?.json()) as ConverseProfile; + if (converseData) { + data.converseUsername = + converseData.formattedName || + converseData.name || + data.converseUsername; + data.address = + converseData.address?.toLowerCase() || data.address?.toLowerCase(); + data.avatar = converseData.avatar || data.avatar; + data.converseEndpoint = converseEndpoint; + } + } catch (error) { + console.error("Failed to fetch Converse profile:", error); + } + + data.preferredName = data.ensDomain || data.converseUsername || "Friend"; + userInfoCache.set(keyToUse, data); + return data; + } +}; + +const fetchWithTimeout = async ( + url: string, + options: RequestInit, + timeout = 5000, +) => { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }); + clearTimeout(id); + return response; + } catch (error) { + clearTimeout(id); + console.error("fetching"); + } +}; diff --git a/packages/framekit/src/components/Chat.tsx b/packages/framekit/src/components/Chat.tsx index 3f752a5da..d9fdd9d17 100644 --- a/packages/framekit/src/components/Chat.tsx +++ b/packages/framekit/src/components/Chat.tsx @@ -1,21 +1,18 @@ +import React from "react"; import { useState, useEffect } from "react"; import { Client as V2Client } from "@xmtp/xmtp-js"; import { Wallet } from "ethers"; import styles from "./Chat.module.css"; -import { getUserInfo, UserInfo } from "@/app/utils/resolver"; +import { UserInfo } from "@/app/utils/resolver"; import { isAddress } from "viem"; -interface ChatProps { - recipientAddress: string; -} - interface Message { id: string; content: string; sender: string; } -export default function Chat({ recipientAddress }: ChatProps) { +function Chat({ user }: { user: UserInfo }) { const [messages, setMessages] = useState([]); const [newMessage, setNewMessage] = useState(""); const [wallet, setWallet] = useState(undefined); @@ -28,15 +25,17 @@ export default function Chat({ recipientAddress }: ChatProps) { const [processedMessageIds] = useState(new Set()); useEffect(() => { + console.log("useEffect triggered with user:", user); + const init = async () => { const newWallet = Wallet.createRandom(); setWallet(newWallet); try { - const userInfo = await getUserInfo(recipientAddress); - setRecipientInfo(userInfo); - - if (userInfo?.address) { + setRecipientInfo(user); + console.log("User info:", user); + if (user?.address) { + console.log("Initializing XMTP with address:", user.address); await initXmtp(newWallet); } else { console.error("Could not resolve recipient address"); @@ -49,7 +48,7 @@ export default function Chat({ recipientAddress }: ChatProps) { }; init(); - }, [recipientAddress]); + }, [user.address]); useEffect(() => { const initConversation = async () => { @@ -160,6 +159,20 @@ export default function Chat({ recipientAddress }: ChatProps) { } }; + const renderMessageContent = (content: string) => { + const urlRegex = /(https?:\/\/[^\s]+)/g; + return content.split(urlRegex).map((part, index) => { + if (urlRegex.test(part)) { + return ( + + {part} + + ); + } + return part; + }); + }; + return (
@@ -170,19 +183,17 @@ export default function Chat({ recipientAddress }: ChatProps) {
*/}
Agent: - {recipientInfo?.preferredName || - (isAddress(recipientAddress) - ? recipientAddress.slice(0, 6) + - "..." + - recipientAddress.slice(-4) - : recipientAddress)} + {user?.preferredName || + (user?.address && isAddress(user.address) + ? user.address.slice(0, 6) + "..." + user.address.slice(-4) + : user?.address)}
{messages.map((msg, index) => (
{msg.sender} - {msg.content} + {renderMessageContent(msg.content)}
))}
@@ -209,3 +220,5 @@ export default function Chat({ recipientAddress }: ChatProps) { ); } + +export default React.memo(Chat); diff --git a/packages/message-kit/package.json b/packages/message-kit/package.json index ec6c3f8fb..b2b1d9e11 100644 --- a/packages/message-kit/package.json +++ b/packages/message-kit/package.json @@ -92,4 +92,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} \ No newline at end of file +} diff --git a/packages/message-kit/src/index.ts b/packages/message-kit/src/index.ts index c42e9a64b..8fd7a8120 100644 --- a/packages/message-kit/src/index.ts +++ b/packages/message-kit/src/index.ts @@ -5,7 +5,7 @@ export * from "./lib/skills.js"; export * from "./helpers/types.js"; export * from "./plugins/gpt.js"; export * from "./plugins/resolver.js"; -export * from "./plugins/framekit.js"; +export * from "./plugins/FrameKit.js"; export * from "./skills/concierge.js"; export * from "./plugins/xmtp.js"; export { Client as V2Client } from "@xmtp/xmtp-js"; diff --git a/packages/message-kit/src/lib/core.ts b/packages/message-kit/src/lib/core.ts index e829c704b..1e318df7f 100644 --- a/packages/message-kit/src/lib/core.ts +++ b/packages/message-kit/src/lib/core.ts @@ -40,7 +40,6 @@ import { } from "@xmtp/content-type-read-receipt"; import { WalletService as CdpWalletService } from "../plugins/cdp.js"; import { WalletService as CircleWalletService } from "../plugins/circle.js"; -import { FrameKit } from "../plugins/framekit.js"; import { executeSkill, parseSkill, findSkill } from "./skills.js"; import { logUserInteraction } from "../helpers/utils.js"; import path from "path"; @@ -69,7 +68,6 @@ export type Context = { agentConfig?: AgentConfig; agent: Agent; walletService: CdpWalletService | CircleWalletService; - framekit: FrameKit; sender?: AbstractedMember; awaitingResponse: boolean; sendAgentMessage: (message: string, metadata: any) => Promise; @@ -100,7 +98,6 @@ export type Context = { export class MessageKit implements Context { xmtp!: XmtpPlugin; storage!: LocalStorage; - framekit!: FrameKit; // Using ! since we know it will be initialized refConv: Conversation | V2Conversation | undefined = undefined; originalMessage: DecodedMessage | DecodedMessageV2 | undefined = undefined; message!: MessageAbstracted; // A message from XMTP abstracted for agent use; @@ -298,7 +295,6 @@ export class MessageKit implements Context { }; //Plugins - context.framekit = new FrameKit(context as unknown as Context); context.xmtp = new XmtpPlugin(context as unknown as Context); //test if (context.agentConfig?.walletService === true) { diff --git a/packages/message-kit/src/plugins/framekit.ts b/packages/message-kit/src/plugins/framekit.ts index 3438ddc6f..b824e1f5d 100644 --- a/packages/message-kit/src/plugins/framekit.ts +++ b/packages/message-kit/src/plugins/framekit.ts @@ -1,5 +1,4 @@ -import { Context } from "../lib/core.js"; -import { getUserInfo } from "../plugins/resolver.js"; +import { getUserInfo } from "./resolver.js"; export interface Frame { title: string; @@ -13,59 +12,61 @@ const framesUrl = : "https://frames.message-kit.org"; export class FrameKit { - private context: Context; - - constructor(context: Context) { - this.context = context; - } - - async sendWallet( + static async sendWallet( ownerAddress: string, agentAddress: string, balance: number, - ) { + ): Promise { let url = `${framesUrl}/wallet?networkId=${"base"}&agentAddress=${agentAddress}&ownerAddress=${ownerAddress}&balance=${balance}`; - await this.context.send(url); + return url; + } + + static async coinbaseLink(address: string): Promise { + let url = `${framesUrl}/coinbase?address=${address}`; + return url; } - async requestPayment( + static async requestPayment( to: string = "humanagent.eth", amount: number = 0.01, token: string = "usdc", onRampURL?: string, - ) { + ): Promise { let senderInfo = await getUserInfo(to); if (!senderInfo) { console.error("Failed to get sender info"); - return; + return ""; } let sendUrl = `${framesUrl}/payment?networkId=${"base"}&amount=${amount}&token=${token}&recipientAddress=${senderInfo?.address}`; if (onRampURL) { sendUrl = sendUrl + "&onRampURL=" + encodeURIComponent(onRampURL); } - await this.context.dm(sendUrl); + return sendUrl; } - async sendReceipt(txLink: string, amount: number) { - if (!txLink) return; + static async sendReceipt(txLink: string, amount: number): Promise { + if (!txLink) return ""; let receiptUrl = `${framesUrl}/receipt?networkId=${"base"}&txLink=${txLink}&amount=${amount}`; - await this.context.dm(receiptUrl); + return receiptUrl; } - async sendConverseDmFrame(peer: string, pretext?: string) { + static async converseLink(peer: string, pretext?: string): Promise { let url = `https://converse.xyz/dm/${peer}`; if (pretext) url += `&pretext=${encodeURIComponent(pretext)}`; - await this.context.send(url); + return url; } - async sendConverseGroupFrame(groupId: string, pretext?: string) { + static async converseGroup( + groupId: string, + pretext?: string, + ): Promise { let url = `https://converse.xyz/group/${groupId}`; if (pretext) url += `&pretext=${encodeURIComponent(pretext)}`; - await this.context.send(url); + return url; } - async sendCustomFrame(frame: Frame) { + static async sendCustomFrame(frame: Frame): Promise { const params = new URLSearchParams(); for (const [key, value] of Object.entries(frame)) { params.append( @@ -75,6 +76,6 @@ export class FrameKit { } const frameUrl = `${framesUrl}/custom?${params.toString()}`; - await this.context.send(frameUrl); + return frameUrl; } } diff --git a/packages/message-kit/src/plugins/resolver.ts b/packages/message-kit/src/plugins/resolver.ts index 5cec749d8..0d938c1e5 100644 --- a/packages/message-kit/src/plugins/resolver.ts +++ b/packages/message-kit/src/plugins/resolver.ts @@ -1,7 +1,4 @@ import { isAddress } from "viem"; -import { V2Client, V3Client } from "../index"; -import { Context } from "../lib/core"; - export const converseEndpointURL = "https://converse.xyz/profile/"; export type InfoCache = Map; @@ -77,7 +74,6 @@ export const userInfoCache = UserInfoCache.getInstance(); export const getUserInfo = async ( key: string, clientAddress?: string, - context?: Context, ): Promise => { let data: UserInfo = { ensDomain: undefined, @@ -129,13 +125,6 @@ export const getUserInfo = async ( console.log("Unable to determine a valid key for fetching user info."); return data; } else { - // Notify user about the fetching process - if (context) { - await context.send( - "Hey there! Give me a sec while I fetch info about you first...", - ); - } - // Fetch data based on ENS domain // Fetch ENS data try { const response = await fetch(`https://ensdata.net/${keyToUse}`); diff --git a/packages/message-kit/src/skills/concierge.ts b/packages/message-kit/src/skills/concierge.ts index 98d5f7176..0300ef4ee 100644 --- a/packages/message-kit/src/skills/concierge.ts +++ b/packages/message-kit/src/skills/concierge.ts @@ -3,6 +3,7 @@ import { Skill } from "../helpers/types"; import { Context } from "../lib/core"; import { getUserInfo } from "../plugins/resolver"; import { isAddress } from "viem"; +import { FrameKit } from "../plugins/FrameKit"; export const concierge: Skill[] = [ { @@ -99,32 +100,33 @@ export async function handleWallet(context: Context) { await context.reply("Check your DM's"); return; } else if (skill === "help") { - await context.send("Im your personal assistant. How can I help you today?"); + await context.dm("Im your personal assistant. How can I help you today?"); } else if (skill === "address") { const walletExist = await walletService.getWallet(sender.address); if (walletExist) { const { balance } = await walletService.checkBalance(sender.address); - await context.send("Your agent wallet address"); - await context.framekit.sendWallet( + await context.dm("Your agent wallet address"); + const url = await FrameKit.sendWallet( walletExist.address, walletExist.agent_address, balance, ); + await context.dm(url); return; } await context.reply("You don't have an agent wallet."); } else if (skill === "balance") { const { balance } = await walletService.checkBalance(sender.address); - await context.send(`Your agent wallet has a balance of $${balance}`); + await context.dm(`Your agent wallet has a balance of $${balance}`); } else if (skill === "fund") { await fund(context, amount); return; } else if (skill === "withdraw") { await withdraw(context, amount); } else if (skill === "swap") { - context.send("I cant do that yet"); + context.dm("I cant do that yet"); // await walletService.swap(sender.address, fromToken, toToken, amount); - // await context.send("Swap completed"); + // await context.dm("Swap completed"); // return; } else if (skill === "transfer") { const { balance } = await walletService.checkBalance(sender.address); @@ -137,7 +139,7 @@ export async function handleWallet(context: Context) { await context.reply("User not found."); return; } - await context.send( + await context.dm( `Transferring ${amount} USDC to ${recipient?.preferredName}`, ); const tx = await walletService.transfer( @@ -166,20 +168,23 @@ async function notifyUser( if (transaction) { await context.dm(`Transfer completed successfully`); if (transaction.getTransactionHash !== undefined) { - await context.framekit.sendReceipt( + const url = await FrameKit.sendReceipt( `https://basescan.org/tx/${transaction.getTransactionHash()}`, amount, ); + await context.dm(url); } else if (transaction.txHash !== undefined) { - await context.framekit.sendReceipt( + const url = await FrameKit.sendReceipt( `https://basescan.org/tx/${transaction.txHash}`, amount, ); + await context.dm(url); } else if (transaction.getTransaction !== undefined) { - await context.framekit.sendReceipt( + const url = await FrameKit.sendReceipt( `https://basescan.org/tx/${transaction.getTransaction()}`, amount, ); + await context.dm(url); } } await context.dm(`Your balance was deducted by $${amount}`); @@ -231,12 +236,13 @@ async function fund( walletData.agent_address, ); await context.dm("Here is the payment link:"); - await context.framekit.requestPayment( + const url = await FrameKit.requestPayment( walletData.agent_address, amount, "USDC", onRamp ? onRampURL : undefined, ); + await context.dm(url); return true; } else { await context.dm("Wrong amount. Max 10 USDC."); @@ -259,12 +265,13 @@ async function fund( walletData.agent_address, ); - await context.framekit.requestPayment( + const url = await FrameKit.requestPayment( walletData.agent_address, Number(response), "USDC", onRamp ? onRampURL : undefined, ); + await context.dm(url); return true; } } diff --git a/templates/ens/src/skills/pay.ts b/templates/ens/src/skills/pay.ts index 765c854b7..333228a15 100644 --- a/templates/ens/src/skills/pay.ts +++ b/templates/ens/src/skills/pay.ts @@ -1,4 +1,4 @@ -import { Context, getUserInfo, Skill } from "@xmtp/message-kit"; +import { Context, FrameKit, getUserInfo, Skill } from "@xmtp/message-kit"; export const pay: Skill[] = [ { @@ -51,8 +51,10 @@ export async function handler(context: Context) { let receiverAddress = username?.address; if (skill === "tip") { let tipAmount = 1; - await context.framekit.requestPayment(receiverAddress, tipAmount); + const url = await FrameKit.requestPayment(receiverAddress, tipAmount); + await context.dm(url); } else if (skill === "pay") { - await context.framekit.requestPayment(receiverAddress, amount, token); + const url = await FrameKit.requestPayment(receiverAddress, amount, token); + await context.dm(url); } } diff --git a/templates/paymentagent/example_prompt.md b/templates/paymentagent/example_prompt.md index 7c39a264f..c5665c0eb 100644 --- a/templates/paymentagent/example_prompt.md +++ b/templates/paymentagent/example_prompt.md @@ -13,18 +13,17 @@ Vibe: A high-energy, risk-embracing personality from the crypto trading world. T - Do not make guesses or assumptions - Only answer if the verified information is in the prompt. - Focus only on helping users with operations detailed below. -- Date: Mon, 16 Dec 2024 15:46:53 GMT, +- Date: Mon, 16 Dec 2024 21:41:44 GMT, ## User context - Start by fetch their domain from or Converse username - Call the user by their name or domain, in case they have one - Ask for a name (if they don't have one) so you can suggest domains. -- Message sent date: 2024-12-16T15:47:16.101Z -- Users address is: 0x93e2fc3e99dfb1238eb9e0ef2580efc5809c7204 -- Users name is: humanagent.eth -- User ENS domain is: humanagent.eth -- Converse username is: Fabri +- Message sent date: 2024-12-16T21:42:17.162Z +- Users address is: 0x40f08f0f853d1c42c61815652b7ccd5a50f0be09 +- Users name is: ArizonaOregon +- Converse username is: ArizonaOregon ## Commands /fund [amount] - Fund your agent wallet. Asume its always usdc. There is no minum to fund the account. Max to top the account is 10 usdc diff --git a/templates/playground/src/skills/cash.ts b/templates/playground/src/skills/cash.ts index 15de8195b..d590081ab 100644 --- a/templates/playground/src/skills/cash.ts +++ b/templates/playground/src/skills/cash.ts @@ -1,4 +1,4 @@ -import { Context } from "@xmtp/message-kit"; +import { Context, FrameKit } from "@xmtp/message-kit"; import type { Skill } from "@xmtp/message-kit"; import { USDCWallet } from "../plugins/usdc.js"; @@ -93,7 +93,11 @@ async function fundHandler(context: Context) { return; } - await context.framekit.requestPayment(usdcWallet.agentAddress, fundAmount); + const url = await FrameKit.requestPayment( + usdcWallet.agentAddress, + fundAmount, + ); + await context.dm(url); await context.send( "After funding, let me know so I can check your balance.", ); diff --git a/templates/playground/src/skills/drip.ts b/templates/playground/src/skills/drip.ts index 3d2206e95..a433da6f2 100644 --- a/templates/playground/src/skills/drip.ts +++ b/templates/playground/src/skills/drip.ts @@ -1,4 +1,4 @@ -import { Context } from "@xmtp/message-kit"; +import { Context, FrameKit } from "@xmtp/message-kit"; import type { Skill } from "@xmtp/message-kit"; import { getRedisClient } from "../plugins/redis.js"; import { LearnWeb3Client, Network } from "../plugins/learnweb3.js"; @@ -77,9 +77,10 @@ export async function handler(context: Context) { } await context.send("Here's your transaction receipt:"); - await context.framekit.sendReceipt( + const url = await FrameKit.sendReceipt( result.value!, selectedNetwork.dripAmount as number, ); + await context.dm(url); return; } diff --git a/templates/playground/src/skills/pay.ts b/templates/playground/src/skills/pay.ts index 8b965e6a2..b78e0958c 100644 --- a/templates/playground/src/skills/pay.ts +++ b/templates/playground/src/skills/pay.ts @@ -1,4 +1,4 @@ -import { Context } from "@xmtp/message-kit"; +import { Context, FrameKit } from "@xmtp/message-kit"; import type { Skill } from "@xmtp/message-kit"; export const registerSkill: Skill[] = [ @@ -31,9 +31,10 @@ export async function handler(context: Context) { const { amount: amountSend, token: tokenSend, username } = params; - await context.framekit.requestPayment( + const url = await FrameKit.requestPayment( username?.address, amountSend, tokenSend, ); + await context.dm(url); } diff --git a/templates/playground/src/skills/token.ts b/templates/playground/src/skills/token.ts index bb7459c8a..3f4fb01b8 100644 --- a/templates/playground/src/skills/token.ts +++ b/templates/playground/src/skills/token.ts @@ -1,5 +1,6 @@ import { Context } from "@xmtp/message-kit"; import type { Skill } from "@xmtp/message-kit"; +import { FrameKit } from "@xmtp/message-kit"; export const token: Skill[] = [ { @@ -53,5 +54,6 @@ export async function handler(context: Context) { ], image: tokenInfo.image, }; - await context.framekit.sendCustomFrame(frame); + const url = await FrameKit.sendCustomFrame(frame); + await context.dm(url); } From 2502f9bdc4640d0fb5d42434aa0563d9feb20c1a Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 00:50:28 -0300 Subject: [PATCH 07/31] frmaekit --- .../framekit/src/app/dm/[address]/page.tsx | 25 ++++++------ packages/framekit/src/app/page.tsx | 15 ------- .../framekit/src/components/Chat.module.css | 26 ++++++++++++- packages/framekit/src/components/Chat.tsx | 39 ++++++++++++++++--- .../framekit/src/components/PaymentFrame.tsx | 16 -------- .../src/components/ReceiptGenerator.tsx | 16 -------- .../framekit/src/components/UrlGenerator.tsx | 16 -------- 7 files changed, 70 insertions(+), 83 deletions(-) diff --git a/packages/framekit/src/app/dm/[address]/page.tsx b/packages/framekit/src/app/dm/[address]/page.tsx index 55e51369e..515ff6e74 100644 --- a/packages/framekit/src/app/dm/[address]/page.tsx +++ b/packages/framekit/src/app/dm/[address]/page.tsx @@ -1,9 +1,9 @@ "use client"; -import { Suspense, useEffect, useState, useMemo } from "react"; +import { Suspense, useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { useParams } from "next/navigation"; import sdk, { type FrameContext } from "@farcaster/frame-sdk"; -import { getUserInfo, UserInfo } from "@/app/utils/resolver"; +import { getUserInfo, type UserInfo } from "@/app/utils/resolver"; const Chat = dynamic(() => import("../../../components/Chat"), { ssr: false, @@ -20,6 +20,7 @@ export default function ChatFrame(): JSX.Element { console.log("Fetching user info for address:", params?.address); const userInfo = await getUserInfo(params?.address as string); console.log("Fetched user info:", userInfo); + setUser(userInfo ?? null); } catch (error) { console.error("Error fetching user info:", error); @@ -27,12 +28,9 @@ export default function ChatFrame(): JSX.Element { setLoading(false); } }; - fetchUserInfo(); }, [params?.address]); - const memoizedUser = useMemo(() => user, [user]) as UserInfo; - if (loading) { return
Loading...
; } @@ -42,9 +40,9 @@ export default function ChatFrame(): JSX.Element { } return ( - + Loading...}> - + ); @@ -83,18 +81,17 @@ function FrameHTML({ function ChatContent({ user }: { user: UserInfo }): JSX.Element { const [isSDKLoaded, setIsSDKLoaded] = useState(false); const [context, setContext] = useState(); - const params = useParams(); useEffect(() => { const initFrame = async () => { - if (!isSDKLoaded) { - setContext(await sdk.context); - sdk.actions.ready(); - setIsSDKLoaded(true); - } + setContext(await sdk.context); + sdk.actions.ready(); }; - initFrame(); + if (sdk && !isSDKLoaded) { + setIsSDKLoaded(true); + initFrame(); + } }, [isSDKLoaded]); return ( diff --git a/packages/framekit/src/app/page.tsx b/packages/framekit/src/app/page.tsx index 718d94a21..b17bbc911 100644 --- a/packages/framekit/src/app/page.tsx +++ b/packages/framekit/src/app/page.tsx @@ -65,21 +65,6 @@ export default function Home() { Custom -
- Powered by{" "} - - MessageKit - -
diff --git a/packages/framekit/src/components/Chat.module.css b/packages/framekit/src/components/Chat.module.css index fb903fba0..529d1bce5 100644 --- a/packages/framekit/src/components/Chat.module.css +++ b/packages/framekit/src/components/Chat.module.css @@ -98,7 +98,6 @@ background-color: #fa6977; color: white; border: none; - padding: 0 1.25rem; cursor: pointer; font-family: monospace; height: 48px; @@ -173,3 +172,28 @@ min-height: 0; } } + +.urlButton { + background-color: #3b82f6; + color: white; + padding: 4px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + transition: background-color 0.2s; + margin: 0 4px; + vertical-align: middle; + line-height: 1.2; + display: inline-flex; + align-items: center; +} + +.urlButton:hover { + background-color: #2563eb; +} + +.urlButton:disabled { + background-color: #9ca3af; + cursor: not-allowed; +} diff --git a/packages/framekit/src/components/Chat.tsx b/packages/framekit/src/components/Chat.tsx index d9fdd9d17..338a52747 100644 --- a/packages/framekit/src/components/Chat.tsx +++ b/packages/framekit/src/components/Chat.tsx @@ -1,10 +1,12 @@ -import React from "react"; +import React, { useCallback } from "react"; import { useState, useEffect } from "react"; import { Client as V2Client } from "@xmtp/xmtp-js"; import { Wallet } from "ethers"; import styles from "./Chat.module.css"; import { UserInfo } from "@/app/utils/resolver"; -import { isAddress } from "viem"; +import { isAddress, parseUnits } from "viem"; +import { extractFrameChain } from "@/app/utils/networks"; +import sdk from "@farcaster/frame-sdk"; interface Message { id: string; @@ -86,6 +88,20 @@ function Chat({ user }: { user: UserInfo }) { initConversation(); }, [xmtp, recipientInfo, wallet]); + const ethereumURL = (url: string) => { + //frames.message-kit.org/payment?networkId=base&amount=0.01&token=USDC&recipientAddress=0x5d8407cb37f12b8c2a7fbb81d182eafa784022ed + + const urlParams = new URLSearchParams(url.split("?")[1]); + const networkId = urlParams.get("networkId"); + const { chainId, tokenAddress } = extractFrameChain(networkId as string); + const amount = urlParams.get("amount"); + const recipientAddress = urlParams.get("recipientAddress"); + + const amountUint256 = parseUnits(amount as string, 6); + const ethereumUrl = `ethereum:${tokenAddress}@${chainId}/transfer?address=${recipientAddress}&uint256=${amountUint256}`; + + return ethereumUrl; + }; useEffect(() => { const streamMessages = async () => { if (!conversation) return; @@ -158,15 +174,28 @@ function Chat({ user }: { user: UserInfo }) { console.error("Error sending message:", error); } }; + const openUrl = useCallback( + (url: string) => { + sdk.actions.openUrl(url); + }, + [newMessage], + ); const renderMessageContent = (content: string) => { const urlRegex = /(https?:\/\/[^\s]+)/g; return content.split(urlRegex).map((part, index) => { if (urlRegex.test(part)) { return ( - - {part} - + ); } return part; diff --git a/packages/framekit/src/components/PaymentFrame.tsx b/packages/framekit/src/components/PaymentFrame.tsx index 2611183ea..d18efacc5 100644 --- a/packages/framekit/src/components/PaymentFrame.tsx +++ b/packages/framekit/src/components/PaymentFrame.tsx @@ -97,22 +97,6 @@ const PaymentFrame: React.FC = ({ url, image, label }) => { -
- Powered by{" "} - - MessageKit - -
); diff --git a/packages/framekit/src/components/ReceiptGenerator.tsx b/packages/framekit/src/components/ReceiptGenerator.tsx index 65ed60adf..a3d3d9c19 100644 --- a/packages/framekit/src/components/ReceiptGenerator.tsx +++ b/packages/framekit/src/components/ReceiptGenerator.tsx @@ -123,22 +123,6 @@ export default function ReceiptGenerator() { )} - -
- Powered by{" "} - - MessageKit - -
); diff --git a/packages/framekit/src/components/UrlGenerator.tsx b/packages/framekit/src/components/UrlGenerator.tsx index 975472d6d..69d5478aa 100644 --- a/packages/framekit/src/components/UrlGenerator.tsx +++ b/packages/framekit/src/components/UrlGenerator.tsx @@ -129,22 +129,6 @@ export default function UrlGenerator({ params }: { params: any }) { )} - -
- Powered by{" "} - - MessageKit - -
); From cbf1ee01bbd91ec8cdfb3c47efb70fd327be0fbf Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 00:54:39 -0300 Subject: [PATCH 08/31] deploy --- packages/message-kit/src/index.ts | 2 +- packages/message-kit/src/skills/concierge.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/message-kit/src/index.ts b/packages/message-kit/src/index.ts index 8fd7a8120..c42e9a64b 100644 --- a/packages/message-kit/src/index.ts +++ b/packages/message-kit/src/index.ts @@ -5,7 +5,7 @@ export * from "./lib/skills.js"; export * from "./helpers/types.js"; export * from "./plugins/gpt.js"; export * from "./plugins/resolver.js"; -export * from "./plugins/FrameKit.js"; +export * from "./plugins/framekit.js"; export * from "./skills/concierge.js"; export * from "./plugins/xmtp.js"; export { Client as V2Client } from "@xmtp/xmtp-js"; diff --git a/packages/message-kit/src/skills/concierge.ts b/packages/message-kit/src/skills/concierge.ts index 0300ef4ee..eeb4b0d01 100644 --- a/packages/message-kit/src/skills/concierge.ts +++ b/packages/message-kit/src/skills/concierge.ts @@ -3,7 +3,7 @@ import { Skill } from "../helpers/types"; import { Context } from "../lib/core"; import { getUserInfo } from "../plugins/resolver"; import { isAddress } from "viem"; -import { FrameKit } from "../plugins/FrameKit"; +import { FrameKit } from "../plugins/framekit"; export const concierge: Skill[] = [ { From a8190fee34baef8c8fa4a2d04605d0e475e3eed6 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:00:33 -0300 Subject: [PATCH 09/31] deploy --- packages/framekit/src/app/utils/resolver.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/framekit/src/app/utils/resolver.ts b/packages/framekit/src/app/utils/resolver.ts index 0d938c1e5..be05b33c4 100644 --- a/packages/framekit/src/app/utils/resolver.ts +++ b/packages/framekit/src/app/utils/resolver.ts @@ -157,6 +157,8 @@ export const getUserInfo = async ( Accept: "application/json", }, body: JSON.stringify({ peer: username }), + mode: "cors", + credentials: "include", }, 5000, ); From 1b5ed9940c5874cbd623ad3e1e2f2ec7b62332aa Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:04:08 -0300 Subject: [PATCH 10/31] deploy --- packages/framekit/src/app/utils/resolver.ts | 72 ++++++++++----------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/framekit/src/app/utils/resolver.ts b/packages/framekit/src/app/utils/resolver.ts index be05b33c4..7f986571e 100644 --- a/packages/framekit/src/app/utils/resolver.ts +++ b/packages/framekit/src/app/utils/resolver.ts @@ -145,42 +145,42 @@ export const getUserInfo = async ( console.error(`Failed to fetch ENS data for ${keyToUse}`); } //Converse profile - try { - const username = keyToUse.replace("@", ""); - const converseEndpoint = `${converseEndpointURL}${username}`; - const response = await fetchWithTimeout( - converseEndpoint, - { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify({ peer: username }), - mode: "cors", - credentials: "include", - }, - 5000, - ); - if (!response?.ok) { - console.error( - `Converse profile request failed with status ${response?.status}`, - ); - } - const converseData = (await response?.json()) as ConverseProfile; - if (converseData) { - data.converseUsername = - converseData.formattedName || - converseData.name || - data.converseUsername; - data.address = - converseData.address?.toLowerCase() || data.address?.toLowerCase(); - data.avatar = converseData.avatar || data.avatar; - data.converseEndpoint = converseEndpoint; - } - } catch (error) { - console.error("Failed to fetch Converse profile:", error); - } + // try { + // const username = keyToUse.replace("@", ""); + // const converseEndpoint = `${converseEndpointURL}${username}`; + // const response = await fetchWithTimeout( + // converseEndpoint, + // { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // Accept: "application/json", + // }, + // body: JSON.stringify({ peer: username }), + // mode: "cors", + // credentials: "include", + // }, + // 5000, + // ); + // if (!response?.ok) { + // console.error( + // `Converse profile request failed with status ${response?.status}`, + // ); + // } + // const converseData = (await response?.json()) as ConverseProfile; + // if (converseData) { + // data.converseUsername = + // converseData.formattedName || + // converseData.name || + // data.converseUsername; + // data.address = + // converseData.address?.toLowerCase() || data.address?.toLowerCase(); + // data.avatar = converseData.avatar || data.avatar; + // data.converseEndpoint = converseEndpoint; + // } + // } catch (error) { + // console.error("Failed to fetch Converse profile:", error); + // } data.preferredName = data.ensDomain || data.converseUsername || "Friend"; userInfoCache.set(keyToUse, data); From 59dd57c63d2027cbd8f0c73ab9d560b6e202c9ed Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:12:34 -0300 Subject: [PATCH 11/31] loading receipt --- packages/framekit/src/app/receipt/page.tsx | 99 +++++++++++++--------- 1 file changed, 61 insertions(+), 38 deletions(-) diff --git a/packages/framekit/src/app/receipt/page.tsx b/packages/framekit/src/app/receipt/page.tsx index 951af4d61..87a98cd4a 100644 --- a/packages/framekit/src/app/receipt/page.tsx +++ b/packages/framekit/src/app/receipt/page.tsx @@ -1,55 +1,78 @@ "use client"; +import React, { useEffect, useState } from "react"; import ReceiptGenerator from "../../components/ReceiptGenerator"; -import { useEffect } from "react"; -export default async function Home({ +export default function Home({ searchParams, }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { - const resolvedSearchParams = await searchParams; - let params = { + const [params, setParams] = useState({ url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: - (resolvedSearchParams.txLink as string) || - (resolvedSearchParams.txlink as string) || - "", - amount: resolvedSearchParams.amount as string, - networkId: - (resolvedSearchParams.networkId as string) || - (resolvedSearchParams.networkid as string) || - "base", - }; + txLink: "", + amount: "", + networkId: "base", + }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function resolveParams() { + const resolvedSearchParams = await searchParams; + setParams({ + url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, + txLink: + (resolvedSearchParams.txLink as string) || + (resolvedSearchParams.txlink as string) || + "", + amount: resolvedSearchParams.amount as string, + networkId: + (resolvedSearchParams.networkId as string) || + (resolvedSearchParams.networkid as string) || + "base", + }); + setLoading(false); + } + resolveParams(); + }, [searchParams]); + const image = `${params.url}/api/receipt?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; useEffect(() => { - // Check if running in browser environment - if (typeof window !== "undefined") { + if (typeof window !== "undefined" && params.txLink) { + console.log("redirecting to", params.txLink); window.location.href = params.txLink; } }, [params.txLink]); - return ( - - - - - - - - - - - - - - - - - - - - - ); + if (loading) { + return
Loading...
; + } + + if (!params.txLink) { + return ( + + + + + + + + + + + + + + + + + + + + + ); + } + + return null; } From 990823c712da1a72405ae9e88d25228f03ceebeb Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:18:20 -0300 Subject: [PATCH 12/31] deploy --- packages/framekit/src/app/receipt/page.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/framekit/src/app/receipt/page.tsx b/packages/framekit/src/app/receipt/page.tsx index 87a98cd4a..45600d80f 100644 --- a/packages/framekit/src/app/receipt/page.tsx +++ b/packages/framekit/src/app/receipt/page.tsx @@ -39,9 +39,16 @@ export default function Home({ const image = `${params.url}/api/receipt?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; useEffect(() => { - if (typeof window !== "undefined" && params.txLink) { - console.log("redirecting to", params.txLink); - window.location.href = params.txLink; + if (typeof window !== "undefined") { + console.log("Window object is available"); + if (params.txLink) { + console.log("Redirecting to", params.txLink); + window.location.href = params.txLink; + } else { + console.log("No txLink found, not redirecting"); + } + } else { + console.log("Window object is not available"); } }, [params.txLink]); From 9c9fad169870273a47642be233349d1c66111723 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:20:12 -0300 Subject: [PATCH 13/31] deploy --- packages/framekit/src/app/receipt/page.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/framekit/src/app/receipt/page.tsx b/packages/framekit/src/app/receipt/page.tsx index 45600d80f..b28f4c487 100644 --- a/packages/framekit/src/app/receipt/page.tsx +++ b/packages/framekit/src/app/receipt/page.tsx @@ -58,7 +58,7 @@ export default function Home({ if (!params.txLink) { return ( - +
@@ -74,10 +74,8 @@ export default function Home({ - - - - + +
); } From 095425f845fa4da376a43bdea4b56da86a14f739 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:33:54 -0300 Subject: [PATCH 14/31] deploy --- .../framekit/src/app/dm/[address]/page.tsx | 54 +++---- packages/framekit/src/app/mint/page.tsx | 112 ++++++++++---- packages/framekit/src/app/page.tsx | 145 ++++++++++-------- packages/framekit/src/app/receipt/page.tsx | 9 +- 4 files changed, 190 insertions(+), 130 deletions(-) diff --git a/packages/framekit/src/app/dm/[address]/page.tsx b/packages/framekit/src/app/dm/[address]/page.tsx index 515ff6e74..d55c4d721 100644 --- a/packages/framekit/src/app/dm/[address]/page.tsx +++ b/packages/framekit/src/app/dm/[address]/page.tsx @@ -1,7 +1,9 @@ "use client"; -import { Suspense, useEffect, useState } from "react"; + +import React, { Suspense, useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { useParams } from "next/navigation"; +import Head from "next/head"; import sdk, { type FrameContext } from "@farcaster/frame-sdk"; import { getUserInfo, type UserInfo } from "@/app/utils/resolver"; @@ -40,11 +42,30 @@ export default function ChatFrame(): JSX.Element { } return ( - - Loading...}> - - - + <> + + + + Chat Frame + + + + + + + + + Loading...}> + + + + ); } @@ -56,26 +77,7 @@ function FrameHTML({ children: React.ReactNode; user: UserInfo; }) { - const baseUrl = `${process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"}`; - const image = `${baseUrl}/api/dm?address=${user.address}`; - - console.log(image); - return ( - - - - - Chat Frame - - - - - - - - {children} - - ); + return <>{children}; } function ChatContent({ user }: { user: UserInfo }): JSX.Element { diff --git a/packages/framekit/src/app/mint/page.tsx b/packages/framekit/src/app/mint/page.tsx index 170b81985..5aacea80b 100644 --- a/packages/framekit/src/app/mint/page.tsx +++ b/packages/framekit/src/app/mint/page.tsx @@ -1,38 +1,84 @@ -export default async function Home({ +"use client"; + +import React, { useEffect, useState } from "react"; +import Head from "next/head"; +import MintComponent from "../../components/MintComponent"; // Assuming there's a MintComponent + +export default function MintPage({ searchParams, }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { - const resolvedSearchParams = await searchParams; // Await the promise - - const url = `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`; - const collectionId = - (resolvedSearchParams.collectionId as string) || - "0x73a333cb82862d4f66f0154229755b184fb4f5b0"; - const tokenId = (resolvedSearchParams.tokenId as string) || "1"; - const mintLink = `ethereum:${collectionId}/mint?uint256=${tokenId}`; - - //ethereum:0x73a333cb82862d4f66f0154229755b184fb4f5b0/mint?uint256=1 - - const image = `${url}/api/mint?collectionId=${collectionId}&tokenId=${tokenId}`; - return ( - - - - - - - - - - - - - - - - - - - ); + const [params, setParams] = useState({ + url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, + txLink: "", + amount: "", + networkId: "base", + }); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function resolveParams() { + const resolvedSearchParams = await searchParams; + setParams({ + url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, + txLink: + (resolvedSearchParams.txLink as string) || + (resolvedSearchParams.txlink as string) || + "", + amount: resolvedSearchParams.amount as string, + networkId: + (resolvedSearchParams.networkId as string) || + (resolvedSearchParams.networkid as string) || + "base", + }); + setLoading(false); + } + resolveParams(); + }, [searchParams]); + + const image = `${params.url}/api/mint?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; + + useEffect(() => { + if (typeof window !== "undefined") { + console.log("Window object is available"); + if (params.txLink) { + console.log("Redirecting to", params.txLink); + window.location.href = params.txLink; + } else { + console.log("No txLink found, not redirecting"); + } + } else { + console.log("Window object is not available"); + } + }, [params.txLink]); + + if (loading) { + return
Loading...
; + } + + if (!params.txLink) { + return ( + <> + + + + + + + + + + + + + + + + + + ); + } + + return null; } diff --git a/packages/framekit/src/app/page.tsx b/packages/framekit/src/app/page.tsx index b17bbc911..c0e015b61 100644 --- a/packages/framekit/src/app/page.tsx +++ b/packages/framekit/src/app/page.tsx @@ -1,73 +1,84 @@ "use client"; -import { GeistMono as geistMono } from "geist/font/mono"; -import { GeistSans as geistSans } from "geist/font/sans"; -export default function Home() { - const url = `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`; - let image = `${url}/hero.jpg`; - return ( - - - - - - - - - - - - - - +import React, { useEffect, useState } from "react"; +import Head from "next/head"; +import MainComponent from "../../components/MainComponent"; // Assuming there's a MainComponent - - - - - -
-
-

frames

+export default function HomePage({ + searchParams, +}: { + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; +}) { + const [params, setParams] = useState({ + url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, + txLink: "", + amount: "", + networkId: "base", + }); + const [loading, setLoading] = useState(true); - - - + useEffect(() => { + async function resolveParams() { + const resolvedSearchParams = await searchParams; + setParams({ + url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, + txLink: + (resolvedSearchParams.txLink as string) || + (resolvedSearchParams.txlink as string) || + "", + amount: resolvedSearchParams.amount as string, + networkId: + (resolvedSearchParams.networkId as string) || + (resolvedSearchParams.networkid as string) || + "base", + }); + setLoading(false); + } + resolveParams(); + }, [searchParams]); - - -
-
- - - ); + const image = `${params.url}/api/home?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; + + useEffect(() => { + if (typeof window !== "undefined") { + console.log("Window object is available"); + if (params.txLink) { + console.log("Redirecting to", params.txLink); + window.location.href = params.txLink; + } else { + console.log("No txLink found, not redirecting"); + } + } else { + console.log("Window object is not available"); + } + }, [params.txLink]); + + if (loading) { + return
Loading...
; + } + + if (!params.txLink) { + return ( + <> + + + + + + + + + + + + + + + + + + ); + } + + return null; } diff --git a/packages/framekit/src/app/receipt/page.tsx b/packages/framekit/src/app/receipt/page.tsx index b28f4c487..93b47f8a8 100644 --- a/packages/framekit/src/app/receipt/page.tsx +++ b/packages/framekit/src/app/receipt/page.tsx @@ -1,6 +1,7 @@ "use client"; import React, { useEffect, useState } from "react"; +import Head from "next/head"; import ReceiptGenerator from "../../components/ReceiptGenerator"; export default function Home({ @@ -58,8 +59,8 @@ export default function Home({ if (!params.txLink) { return ( -
- + <> + @@ -73,9 +74,9 @@ export default function Home({ - + -
+ ); } From 1f5f3c6df27c03f160f8c3142375405b62c4849f Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:40:56 -0300 Subject: [PATCH 15/31] deploy --- packages/framekit/src/app/mint/page.tsx | 110 +++++----------- packages/framekit/src/app/page.tsx | 144 ++++++++++----------- packages/framekit/src/app/payment/page.tsx | 19 +-- 3 files changed, 107 insertions(+), 166 deletions(-) diff --git a/packages/framekit/src/app/mint/page.tsx b/packages/framekit/src/app/mint/page.tsx index 5aacea80b..ccf6e20cb 100644 --- a/packages/framekit/src/app/mint/page.tsx +++ b/packages/framekit/src/app/mint/page.tsx @@ -1,84 +1,40 @@ -"use client"; - -import React, { useEffect, useState } from "react"; import Head from "next/head"; -import MintComponent from "../../components/MintComponent"; // Assuming there's a MintComponent -export default function MintPage({ +export default async function Home({ searchParams, }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { - const [params, setParams] = useState({ - url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: "", - amount: "", - networkId: "base", - }); - const [loading, setLoading] = useState(true); - - useEffect(() => { - async function resolveParams() { - const resolvedSearchParams = await searchParams; - setParams({ - url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: - (resolvedSearchParams.txLink as string) || - (resolvedSearchParams.txlink as string) || - "", - amount: resolvedSearchParams.amount as string, - networkId: - (resolvedSearchParams.networkId as string) || - (resolvedSearchParams.networkid as string) || - "base", - }); - setLoading(false); - } - resolveParams(); - }, [searchParams]); - - const image = `${params.url}/api/mint?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; - - useEffect(() => { - if (typeof window !== "undefined") { - console.log("Window object is available"); - if (params.txLink) { - console.log("Redirecting to", params.txLink); - window.location.href = params.txLink; - } else { - console.log("No txLink found, not redirecting"); - } - } else { - console.log("Window object is not available"); - } - }, [params.txLink]); - - if (loading) { - return
Loading...
; - } - - if (!params.txLink) { - return ( - <> - - - - - - - - - - - - - - - - - - ); - } - - return null; + const resolvedSearchParams = await searchParams; // Await the promise + + const url = `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`; + const collectionId = + (resolvedSearchParams.collectionId as string) || + "0x73a333cb82862d4f66f0154229755b184fb4f5b0"; + const tokenId = (resolvedSearchParams.tokenId as string) || "1"; + const mintLink = `ethereum:${collectionId}/mint?uint256=${tokenId}`; + + //ethereum:0x73a333cb82862d4f66f0154229755b184fb4f5b0/mint?uint256=1 + + const image = `${url}/api/mint?collectionId=${collectionId}&tokenId=${tokenId}`; + return ( + <> + + + + + + + + + + + + + + + +
+ + ); } diff --git a/packages/framekit/src/app/page.tsx b/packages/framekit/src/app/page.tsx index c0e015b61..914c1afd8 100644 --- a/packages/framekit/src/app/page.tsx +++ b/packages/framekit/src/app/page.tsx @@ -1,84 +1,74 @@ "use client"; - -import React, { useEffect, useState } from "react"; +import { GeistMono as geistMono } from "geist/font/mono"; +import { GeistSans as geistSans } from "geist/font/sans"; import Head from "next/head"; -import MainComponent from "../../components/MainComponent"; // Assuming there's a MainComponent - -export default function HomePage({ - searchParams, -}: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { - const [params, setParams] = useState({ - url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: "", - amount: "", - networkId: "base", - }); - const [loading, setLoading] = useState(true); - - useEffect(() => { - async function resolveParams() { - const resolvedSearchParams = await searchParams; - setParams({ - url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: - (resolvedSearchParams.txLink as string) || - (resolvedSearchParams.txlink as string) || - "", - amount: resolvedSearchParams.amount as string, - networkId: - (resolvedSearchParams.networkId as string) || - (resolvedSearchParams.networkid as string) || - "base", - }); - setLoading(false); - } - resolveParams(); - }, [searchParams]); - - const image = `${params.url}/api/home?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; - - useEffect(() => { - if (typeof window !== "undefined") { - console.log("Window object is available"); - if (params.txLink) { - console.log("Redirecting to", params.txLink); - window.location.href = params.txLink; - } else { - console.log("No txLink found, not redirecting"); - } - } else { - console.log("Window object is not available"); - } - }, [params.txLink]); - if (loading) { - return
Loading...
; - } +export default function Home() { + const url = `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`; + let image = `${url}/hero.jpg`; + return ( + <> + + + + + + + + + + + + + - if (!params.txLink) { - return ( - <> - - - - - - - - - - + + + + + +
+
+

frames

- - - - - - - ); - } + + + - return null; + + +
+
+ + + ); } diff --git a/packages/framekit/src/app/payment/page.tsx b/packages/framekit/src/app/payment/page.tsx index a78d1d72e..6f1e21d66 100644 --- a/packages/framekit/src/app/payment/page.tsx +++ b/packages/framekit/src/app/payment/page.tsx @@ -1,6 +1,7 @@ import { parseUnits } from "viem"; import PaymentFrame from "../../components/PaymentFrame"; import { extractFrameChain } from "../utils/networks"; +import Head from "next/head"; export default async function Home({ searchParams, @@ -28,14 +29,8 @@ export default async function Home({ const image = `${params.url}/api/payment?networkId=${params.networkId}&amount=${params.amount}&recipientAddress=${params.recipientAddress}`; return ( - - + <> + @@ -99,8 +94,8 @@ export default async function Home({ } `} - - +
- - +
+ ); } From 070f67fde48f9e3f58d40d4c17a1ea034f5e58ed Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:46:02 -0300 Subject: [PATCH 16/31] Head html --- packages/framekit/src/app/custom/page.tsx | 10 ++++++---- packages/framekit/src/app/generator/page.tsx | 5 ++--- packages/framekit/src/app/layout.tsx | 10 +++++++++- packages/framekit/src/app/wallet/page.tsx | 19 +++++++------------ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/framekit/src/app/custom/page.tsx b/packages/framekit/src/app/custom/page.tsx index b031b8426..8bc5088bb 100644 --- a/packages/framekit/src/app/custom/page.tsx +++ b/packages/framekit/src/app/custom/page.tsx @@ -1,3 +1,5 @@ +import Head from "next/head"; + export default async function Home({ searchParams, }: { @@ -66,15 +68,15 @@ export default async function Home({ )), ]; return ( - - + <> + {metas} - +
{JSON.stringify(metaTags, undefined, 2)}
- + ); } diff --git a/packages/framekit/src/app/generator/page.tsx b/packages/framekit/src/app/generator/page.tsx index 5de026c48..e603e1302 100644 --- a/packages/framekit/src/app/generator/page.tsx +++ b/packages/framekit/src/app/generator/page.tsx @@ -16,11 +16,10 @@ export default async function Home({ onRampURL: resolvedSearchParams?.onRampURL as string, }; return ( - - + <> - + ); } diff --git a/packages/framekit/src/app/layout.tsx b/packages/framekit/src/app/layout.tsx index f9334d3f3..a23ff60f9 100644 --- a/packages/framekit/src/app/layout.tsx +++ b/packages/framekit/src/app/layout.tsx @@ -1,3 +1,4 @@ +import Head from "next/head"; import "./globals.css"; export default function RootLayout({ @@ -5,5 +6,12 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return children; + return ( + + + FrameKit + + {children} + + ); } diff --git a/packages/framekit/src/app/wallet/page.tsx b/packages/framekit/src/app/wallet/page.tsx index 77dc43295..a9c36f38f 100644 --- a/packages/framekit/src/app/wallet/page.tsx +++ b/packages/framekit/src/app/wallet/page.tsx @@ -1,3 +1,4 @@ +import Head from "next/head"; import PaymentFrame from "../../components/PaymentFrame"; import { extractFrameChain } from "../utils/networks"; @@ -34,14 +35,8 @@ export default async function Home({ const image = `${params.url}/api/wallet?networkId=${params.networkId}&agentAddress=${params.agentAddress}&ownerAddress=${params.ownerAddress}&balance=${params.balance}`; return ( - - + <> + @@ -101,8 +96,8 @@ export default async function Home({ } `} - - +
- - +
+ ); } From 7eba9a01fb7abf87216ca2cfc990433eaca47194 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:46:33 -0300 Subject: [PATCH 17/31] Head html --- packages/framekit/src/app/layout.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/framekit/src/app/layout.tsx b/packages/framekit/src/app/layout.tsx index a23ff60f9..ffe4d0233 100644 --- a/packages/framekit/src/app/layout.tsx +++ b/packages/framekit/src/app/layout.tsx @@ -6,12 +6,5 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return ( - - - FrameKit - - {children} - - ); + return {children}; } From 185a4aa40eb3b892cbf70f924c1d71544ea66df7 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:51:19 -0300 Subject: [PATCH 18/31] circle agent --- packages/message-kit/src/plugins/circle.ts | 4 ---- templates/circle-agent/example_prompt.md | 11 +++++++---- .../circle-agent/{src => }/scripts/setupCircle.ts | 0 yarn.lock | 1 - 4 files changed, 7 insertions(+), 9 deletions(-) rename templates/circle-agent/{src => }/scripts/setupCircle.ts (100%) diff --git a/packages/message-kit/src/plugins/circle.ts b/packages/message-kit/src/plugins/circle.ts index fde1373d0..d1554239e 100644 --- a/packages/message-kit/src/plugins/circle.ts +++ b/packages/message-kit/src/plugins/circle.ts @@ -79,10 +79,6 @@ export class WalletService implements AgentWallet { name: "user", refId: normalizedIdentifier, }, - { - name: "senderAddress", - refId: this.senderAddress, - }, ], }); diff --git a/templates/circle-agent/example_prompt.md b/templates/circle-agent/example_prompt.md index 5dcf2d1c1..64c24376b 100644 --- a/templates/circle-agent/example_prompt.md +++ b/templates/circle-agent/example_prompt.md @@ -13,23 +13,24 @@ You are a helpful agent called @bot that lives inside a web3 messaging app calle - Do not make guesses or assumptions - Only answer if the verified information is in the prompt. - Focus only on helping users with operations detailed below. -- Date: Sun, 15 Dec 2024 18:10:10 GMT, +- Date: Tue, 17 Dec 2024 04:50:50 GMT, ## User context - Start by fetch their domain from or Converse username - Call the user by their name or domain, in case they have one - Ask for a name (if they don't have one) so you can suggest domains. -- Message sent date: 2024-12-15T18:10:22.018Z +- Message sent date: 2024-12-17T04:51:03.339Z - Users address is: 0x40f08f0f853d1c42c61815652b7ccd5a50f0be09 - Users name is: ArizonaOregon - Converse username is: ArizonaOregon ## Commands -/fund [amount] - Fund your CDP wallet. +/fund [amount] - Fund your agent wallet. Asume its always usdc. There is no minum to fund the account. Max to top the account is 10 usdc /transfer [recipient] [amount] - Transfer USDC to another user. -/balance - Check your wallet balance. +/balance - Check your USDC wallet balance. /address - Check your agent wallet address/status/balance. Always assume the user is talking about its agent wallet. +/swap [amount] [fromToken] [toToken] - Swap between tokens (e.g., ETH to USDC). ## Examples /fund 10 @@ -47,3 +48,5 @@ You are a helpful agent called @bot that lives inside a web3 messaging app calle /pay vitalik.eth 0.01 /balance /address +/swap 1 eth usdc +/swap 100 usdc eth diff --git a/templates/circle-agent/src/scripts/setupCircle.ts b/templates/circle-agent/scripts/setupCircle.ts similarity index 100% rename from templates/circle-agent/src/scripts/setupCircle.ts rename to templates/circle-agent/scripts/setupCircle.ts diff --git a/yarn.lock b/yarn.lock index f6040081c..24fb95133 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4565,7 +4565,6 @@ __metadata: version: 0.0.0-use.local resolution: "circle-agent@workspace:templates/circle-agent" dependencies: - "@circle-fin/developer-controlled-wallets": "npm:^6.0.0" "@types/node": "npm:^20.14.2" "@xmtp/message-kit": "workspace:*" typescript: "npm:^5.4.5" From ec87a7067656da18d1982390d817f10b9bd22699 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 01:56:53 -0300 Subject: [PATCH 19/31] circle agent --- packages/message-kit/src/plugins/circle.ts | 2 +- yarn.lock | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/message-kit/src/plugins/circle.ts b/packages/message-kit/src/plugins/circle.ts index d1554239e..27bbf5943 100644 --- a/packages/message-kit/src/plugins/circle.ts +++ b/packages/message-kit/src/plugins/circle.ts @@ -81,7 +81,7 @@ export class WalletService implements AgentWallet { }, ], }); - + //test if (!response?.data?.wallets?.[0]) { throw new Error("Failed to create wallet"); } diff --git a/yarn.lock b/yarn.lock index 24fb95133..f6040081c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4565,6 +4565,7 @@ __metadata: version: 0.0.0-use.local resolution: "circle-agent@workspace:templates/circle-agent" dependencies: + "@circle-fin/developer-controlled-wallets": "npm:^6.0.0" "@types/node": "npm:^20.14.2" "@xmtp/message-kit": "workspace:*" typescript: "npm:^5.4.5" From b22618b892940e2903617ed333c51f45c1e34299 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 02:00:55 -0300 Subject: [PATCH 20/31] circle agent --- packages/framekit/src/app/receipt/page.tsx | 32 ++++++++++------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/framekit/src/app/receipt/page.tsx b/packages/framekit/src/app/receipt/page.tsx index 93b47f8a8..a6df333c5 100644 --- a/packages/framekit/src/app/receipt/page.tsx +++ b/packages/framekit/src/app/receipt/page.tsx @@ -7,7 +7,7 @@ import ReceiptGenerator from "../../components/ReceiptGenerator"; export default function Home({ searchParams, }: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; + searchParams: { [key: string]: string | string[] | undefined }; }) { const [params, setParams] = useState({ url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, @@ -18,23 +18,19 @@ export default function Home({ const [loading, setLoading] = useState(true); useEffect(() => { - async function resolveParams() { - const resolvedSearchParams = await searchParams; - setParams({ - url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: - (resolvedSearchParams.txLink as string) || - (resolvedSearchParams.txlink as string) || - "", - amount: resolvedSearchParams.amount as string, - networkId: - (resolvedSearchParams.networkId as string) || - (resolvedSearchParams.networkid as string) || - "base", - }); - setLoading(false); - } - resolveParams(); + setParams({ + url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, + txLink: + (searchParams.txLink as string) || + (searchParams.txlink as string) || + "", + amount: (searchParams.amount as string) || "", + networkId: + (searchParams.networkId as string) || + (searchParams.networkid as string) || + "base", + }); + setLoading(false); }, [searchParams]); const image = `${params.url}/api/receipt?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; From 478befe1d0b412a36805fabb11795cfeedf3e4c3 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 02:07:06 -0300 Subject: [PATCH 21/31] circle agent --- packages/framekit/src/app/receipt/page.tsx | 37 ++++++++++++++-------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/packages/framekit/src/app/receipt/page.tsx b/packages/framekit/src/app/receipt/page.tsx index a6df333c5..faa51dfbc 100644 --- a/packages/framekit/src/app/receipt/page.tsx +++ b/packages/framekit/src/app/receipt/page.tsx @@ -7,7 +7,7 @@ import ReceiptGenerator from "../../components/ReceiptGenerator"; export default function Home({ searchParams, }: { - searchParams: { [key: string]: string | string[] | undefined }; + searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { const [params, setParams] = useState({ url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, @@ -18,19 +18,28 @@ export default function Home({ const [loading, setLoading] = useState(true); useEffect(() => { - setParams({ - url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: - (searchParams.txLink as string) || - (searchParams.txlink as string) || - "", - amount: (searchParams.amount as string) || "", - networkId: - (searchParams.networkId as string) || - (searchParams.networkid as string) || - "base", - }); - setLoading(false); + async function resolveParams() { + try { + const resolvedSearchParams = await searchParams; + setParams({ + url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, + txLink: + (resolvedSearchParams.txLink as string) || + (resolvedSearchParams.txlink as string) || + "", + amount: (resolvedSearchParams.amount as string) || "", + networkId: + (resolvedSearchParams.networkId as string) || + (resolvedSearchParams.networkid as string) || + "base", + }); + } catch (error) { + console.error("Error resolving searchParams:", error); + } finally { + setLoading(false); + } + } + resolveParams(); }, [searchParams]); const image = `${params.url}/api/receipt?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; From 7aeae737798309f15d32a748cfa61d8feaabb1db Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 02:28:34 -0300 Subject: [PATCH 22/31] fixes --- packages/framekit/src/app/layout.tsx | 6 +- packages/framekit/src/app/payment/page.tsx | 138 ++++++++----------- packages/framekit/src/app/receipt/page.tsx | 149 ++++++++++----------- 3 files changed, 132 insertions(+), 161 deletions(-) diff --git a/packages/framekit/src/app/layout.tsx b/packages/framekit/src/app/layout.tsx index ffe4d0233..587a86c43 100644 --- a/packages/framekit/src/app/layout.tsx +++ b/packages/framekit/src/app/layout.tsx @@ -6,5 +6,9 @@ export default function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - return {children}; + return ( + + {children} + + ); } diff --git a/packages/framekit/src/app/payment/page.tsx b/packages/framekit/src/app/payment/page.tsx index 6f1e21d66..b7724a00a 100644 --- a/packages/framekit/src/app/payment/page.tsx +++ b/packages/framekit/src/app/payment/page.tsx @@ -1,15 +1,18 @@ import { parseUnits } from "viem"; import PaymentFrame from "../../components/PaymentFrame"; import { extractFrameChain } from "../utils/networks"; -import Head from "next/head"; +import { Metadata } from "next"; -export default async function Home({ - searchParams, -}: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { +type SearchParams = { [key: string]: string | string[] | undefined }; + +type Props = { + searchParams: Promise; +}; + +// Helper function to safely get search params +async function getParams(searchParams: Promise) { const resolvedSearchParams = await searchParams; - const params = { + return { url: process.env.NEXT_PUBLIC_URL, recipientAddress: (resolvedSearchParams?.recipientAddress as string) || @@ -23,89 +26,56 @@ export default async function Home({ (resolvedSearchParams?.networkid as string) || "base", }; +} + +export async function generateMetadata({ + searchParams, +}: Props): Promise { + const params = await getParams(searchParams); const { chainId, tokenAddress } = extractFrameChain(params.networkId); const amountUint256 = parseUnits(params.amount.toString(), 6); const ethereumUrl = `ethereum:${tokenAddress}@${chainId}/transfer?address=${params.recipientAddress}&uint256=${amountUint256}`; const image = `${params.url}/api/payment?networkId=${params.networkId}&amount=${params.amount}&recipientAddress=${params.recipientAddress}`; - return ( - <> - - - - - - - - - - - - - - - - {params.onRampURL && ( - <> - - - - - )} - - -
- -
- + return ( +
+ +
); } diff --git a/packages/framekit/src/app/receipt/page.tsx b/packages/framekit/src/app/receipt/page.tsx index faa51dfbc..7cb77d4db 100644 --- a/packages/framekit/src/app/receipt/page.tsx +++ b/packages/framekit/src/app/receipt/page.tsx @@ -1,89 +1,86 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import Head from "next/head"; +import { Metadata } from "next"; import ReceiptGenerator from "../../components/ReceiptGenerator"; -export default function Home({ - searchParams, -}: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { - const [params, setParams] = useState({ - url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: "", - amount: "", - networkId: "base", - }); - const [loading, setLoading] = useState(true); +type SearchParams = { [key: string]: string | string[] | undefined }; - useEffect(() => { - async function resolveParams() { - try { - const resolvedSearchParams = await searchParams; - setParams({ - url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, - txLink: - (resolvedSearchParams.txLink as string) || - (resolvedSearchParams.txlink as string) || - "", - amount: (resolvedSearchParams.amount as string) || "", - networkId: - (resolvedSearchParams.networkId as string) || - (resolvedSearchParams.networkid as string) || - "base", - }); - } catch (error) { - console.error("Error resolving searchParams:", error); - } finally { - setLoading(false); - } - } - resolveParams(); - }, [searchParams]); +// Helper function to safely get search params +async function getParams(searchParams: Promise) { + const resolvedSearchParams = await searchParams; + return { + url: `${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}`, + txLink: + String(resolvedSearchParams?.txLink || "") || + String(resolvedSearchParams?.txlink || "") || + "", + amount: String(resolvedSearchParams?.amount || ""), + networkId: + String(resolvedSearchParams?.networkId || "") || + String(resolvedSearchParams?.networkid || "") || + "base", + }; +} +export async function generateMetadata({ + searchParams, +}: { + searchParams: Promise; +}): Promise { + const params = await getParams(searchParams); const image = `${params.url}/api/receipt?txLink=${params.txLink}&amount=${params.amount}&networkId=${params.networkId}`; - useEffect(() => { - if (typeof window !== "undefined") { - console.log("Window object is available"); - if (params.txLink) { - console.log("Redirecting to", params.txLink); - window.location.href = params.txLink; - } else { - console.log("No txLink found, not redirecting"); - } - } else { - console.log("Window object is not available"); - } - }, [params.txLink]); + return { + title: "Ethereum Payment", + other: { + "fc:frame": "vNext", + "of:version": "vNext", + "of:accepts:xmtp": "vNext", + "fc:frame:image": image, + "og:image": image, + "fc:frame:ratio": "1.91:1", + "fc:frame:button:1": "Transaction Receipt", + "fc:frame:button:1:action": "link", + "fc:frame:button:1:target": params.txLink, + }, + }; +} - if (loading) { - return
Loading...
; - } +export default async function ReceiptPage({ + searchParams, +}: { + searchParams: Promise; +}) { + const params = await getParams(searchParams); - if (!params.txLink) { + // If there's a txLink, redirect to it + if (params.txLink && params.txLink !== "") { return ( - <> - - - - - - - - - - - - - - - - - +
+ + Redirecting to transaction... +
); } - return null; + // If no txLink, show the receipt generator + return ( +
+ +
+ ); } From 84077bdde9a22026112c03a29a170e36d04ba589 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 03:05:30 -0300 Subject: [PATCH 23/31] deploy --- packages/framekit/public/hero copy.jpg | Bin 86815 -> 0 bytes packages/framekit/src/app/api/dm/route.tsx | 10 +- .../src/app/dm/[address]/ChatClient.tsx | 84 ++++++++++++++ .../framekit/src/app/dm/[address]/metadata.ts | 45 ++++++++ .../framekit/src/app/dm/[address]/page.tsx | 104 +----------------- 5 files changed, 138 insertions(+), 105 deletions(-) delete mode 100644 packages/framekit/public/hero copy.jpg create mode 100644 packages/framekit/src/app/dm/[address]/ChatClient.tsx create mode 100644 packages/framekit/src/app/dm/[address]/metadata.ts diff --git a/packages/framekit/public/hero copy.jpg b/packages/framekit/public/hero copy.jpg deleted file mode 100644 index fce147d18582a18125f13bf1dfd8caa646a5fb6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86815 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 V^Ur!=qA-7{FvX1*dWC;}{x9g ), { - width: 500, - height: 500, + width: 900, + height: 600, fonts: [ { data: interFontData, @@ -80,7 +80,7 @@ export async function GET(req: NextRequest) { background: "#000000", height: "100%", width: "100%", - padding: "48px", + padding: "60px", }}>
), { - width: 955, - height: 500, + width: 900, + height: 600, fonts: [ { data: interFontData, diff --git a/packages/framekit/src/app/dm/[address]/ChatClient.tsx b/packages/framekit/src/app/dm/[address]/ChatClient.tsx new file mode 100644 index 000000000..91fd09d9b --- /dev/null +++ b/packages/framekit/src/app/dm/[address]/ChatClient.tsx @@ -0,0 +1,84 @@ +"use client"; + +import React, { Suspense, useEffect, useState } from "react"; +import dynamic from "next/dynamic"; +import { useParams } from "next/navigation"; +import sdk, { type FrameContext } from "@farcaster/frame-sdk"; +import { getUserInfo, type UserInfo } from "@/app/utils/resolver"; + +const Chat = dynamic(() => import("../../../components/Chat"), { + ssr: false, +}); + +export function ChatFrameClient(): JSX.Element { + const params = useParams(); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchUserInfo = async () => { + try { + console.log("Fetching user info for address:", params?.address); + const userInfo = await getUserInfo(params?.address as string); + console.log("Fetched user info:", userInfo); + + setUser(userInfo ?? null); + } catch (error) { + console.error("Error fetching user info:", error); + } finally { + setLoading(false); + } + }; + fetchUserInfo(); + }, [params?.address]); + + if (loading) { + return
Loading...
; + } + + if (!user) { + return
User not found
; + } + + return ( + + Loading...
}> + + +
+ ); +} + +// Create a wrapper component that will render the full HTML +function FrameHTML({ + children, + user, +}: { + children: React.ReactNode; + user: UserInfo; +}) { + return <>{children}; +} + +function ChatContent({ user }: { user: UserInfo }): JSX.Element { + const [isSDKLoaded, setIsSDKLoaded] = useState(false); + const [context, setContext] = useState(); + + useEffect(() => { + const initFrame = async () => { + setContext(await sdk.context); + sdk.actions.ready(); + }; + + if (sdk && !isSDKLoaded) { + setIsSDKLoaded(true); + initFrame(); + } + }, [isSDKLoaded]); + + return ( +
+ +
+ ); +} diff --git a/packages/framekit/src/app/dm/[address]/metadata.ts b/packages/framekit/src/app/dm/[address]/metadata.ts new file mode 100644 index 000000000..66a306d04 --- /dev/null +++ b/packages/framekit/src/app/dm/[address]/metadata.ts @@ -0,0 +1,45 @@ +import { getUserInfo } from "@/app/utils/resolver"; +import { Metadata, ResolvedMetadata } from "next"; + +type Props = { + params: Promise<{ address: string }>; +}; + +// Helper function to safely get params +async function getParams(params: Promise<{ address: string }>) { + const resolvedParams = await params; + return { + address: resolvedParams.address || "", + }; +} + +export async function generateMetadata({ params }: Props): Promise { + const resolvedParams = await params; + const userInfo = await getUserInfo(resolvedParams?.address as string); + const imageUrl = `${process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"}/api/dm?address=${userInfo?.address}`; + + return { + title: `Chat with ${userInfo?.preferredName || resolvedParams?.address}`, + other: { + "og:image": imageUrl, + "fc:frame": JSON.stringify({ + version: "next", + imageUrl, + button: { + title: "Start Chat", + action: { + type: "launch_frame", + name: "Chat App", + url: `${process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"}/dm/${userInfo?.address}`, + splashImageUrl: `${process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"}/messagekit-logo.png`, + splashBackgroundColor: "#ffffff", + }, + }, + }), + "of:version": "vNext", + "of:accepts:xmtp": "vNext", + "fc:frame:image": imageUrl, + "fc:frame:button:1": "Start Chat", + }, + }; +} diff --git a/packages/framekit/src/app/dm/[address]/page.tsx b/packages/framekit/src/app/dm/[address]/page.tsx index d55c4d721..eb6bed2d7 100644 --- a/packages/framekit/src/app/dm/[address]/page.tsx +++ b/packages/framekit/src/app/dm/[address]/page.tsx @@ -1,104 +1,8 @@ -"use client"; +import { generateMetadata } from "./metadata"; +import { ChatFrameClient } from "./ChatClient"; -import React, { Suspense, useEffect, useState } from "react"; -import dynamic from "next/dynamic"; -import { useParams } from "next/navigation"; -import Head from "next/head"; -import sdk, { type FrameContext } from "@farcaster/frame-sdk"; -import { getUserInfo, type UserInfo } from "@/app/utils/resolver"; - -const Chat = dynamic(() => import("../../../components/Chat"), { - ssr: false, -}); +export { generateMetadata }; export default function ChatFrame(): JSX.Element { - const params = useParams(); - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const fetchUserInfo = async () => { - try { - console.log("Fetching user info for address:", params?.address); - const userInfo = await getUserInfo(params?.address as string); - console.log("Fetched user info:", userInfo); - - setUser(userInfo ?? null); - } catch (error) { - console.error("Error fetching user info:", error); - } finally { - setLoading(false); - } - }; - fetchUserInfo(); - }, [params?.address]); - - if (loading) { - return
Loading...
; - } - - if (!user) { - return
User not found
; - } - - return ( - <> - - - - Chat Frame - - - - - - - - - Loading...}> - - - - - ); -} - -// Create a wrapper component that will render the full HTML -function FrameHTML({ - children, - user, -}: { - children: React.ReactNode; - user: UserInfo; -}) { - return <>{children}; -} - -function ChatContent({ user }: { user: UserInfo }): JSX.Element { - const [isSDKLoaded, setIsSDKLoaded] = useState(false); - const [context, setContext] = useState(); - - useEffect(() => { - const initFrame = async () => { - setContext(await sdk.context); - sdk.actions.ready(); - }; - - if (sdk && !isSDKLoaded) { - setIsSDKLoaded(true); - initFrame(); - } - }, [isSDKLoaded]); - - return ( -
- -
- ); + return ; } From affd2ef5a7087e49b79b6010eb6dca1255417950 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 03:10:08 -0300 Subject: [PATCH 24/31] deploy --- packages/framekit/src/app/api/dm/route.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/framekit/src/app/api/dm/route.tsx b/packages/framekit/src/app/api/dm/route.tsx index de8cd8bd8..0ece7b36b 100644 --- a/packages/framekit/src/app/api/dm/route.tsx +++ b/packages/framekit/src/app/api/dm/route.tsx @@ -16,6 +16,12 @@ const interSemiboldFontData = fs.readFileSync(interSemiboldFontPath); export async function GET(req: NextRequest) { try { + const headers = new Headers({ + "Cache-Control": "no-cache, no-store, must-revalidate", + Pragma: "no-cache", + Expires: "0", + }); + const address = req.nextUrl.searchParams.get("address") ?? ""; const user = await getUserInfo(address); @@ -66,6 +72,7 @@ export async function GET(req: NextRequest) { weight: 400, }, ], + headers: headers, }, ); } @@ -120,6 +127,7 @@ export async function GET(req: NextRequest) { name: "Inter-SemiBold", }, ], + headers: headers, }, ); } catch (error) { From 370a534e93e45005fec167a174b927a92cef9f10 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 03:13:27 -0300 Subject: [PATCH 25/31] deploy --- packages/framekit/src/app/api/dm/route.tsx | 26 ++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/framekit/src/app/api/dm/route.tsx b/packages/framekit/src/app/api/dm/route.tsx index 0ece7b36b..1e06a300c 100644 --- a/packages/framekit/src/app/api/dm/route.tsx +++ b/packages/framekit/src/app/api/dm/route.tsx @@ -81,34 +81,46 @@ export async function GET(req: NextRequest) {
-
{`Talk to`}
+
{`Talk to`}
-
+
{params.preferredName}
From 9bf13d1f2ce25e3cdf835c277d7fbd51e7e8b008 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 03:16:59 -0300 Subject: [PATCH 26/31] deploy --- packages/framekit/src/app/dm/[address]/metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/framekit/src/app/dm/[address]/metadata.ts b/packages/framekit/src/app/dm/[address]/metadata.ts index 66a306d04..335b49173 100644 --- a/packages/framekit/src/app/dm/[address]/metadata.ts +++ b/packages/framekit/src/app/dm/[address]/metadata.ts @@ -17,7 +17,7 @@ export async function generateMetadata({ params }: Props): Promise { const resolvedParams = await params; const userInfo = await getUserInfo(resolvedParams?.address as string); const imageUrl = `${process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"}/api/dm?address=${userInfo?.address}`; - + console.log("imageUrl", imageUrl); return { title: `Chat with ${userInfo?.preferredName || resolvedParams?.address}`, other: { From 046721ddfc4f9afe3318d5886769dfe68e21aaa3 Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 03:19:32 -0300 Subject: [PATCH 27/31] image --- packages/framekit/src/app/api/{dm => dmimage}/route.tsx | 0 packages/framekit/src/app/dm/[address]/metadata.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/framekit/src/app/api/{dm => dmimage}/route.tsx (100%) diff --git a/packages/framekit/src/app/api/dm/route.tsx b/packages/framekit/src/app/api/dmimage/route.tsx similarity index 100% rename from packages/framekit/src/app/api/dm/route.tsx rename to packages/framekit/src/app/api/dmimage/route.tsx diff --git a/packages/framekit/src/app/dm/[address]/metadata.ts b/packages/framekit/src/app/dm/[address]/metadata.ts index 335b49173..4a4c2dc4b 100644 --- a/packages/framekit/src/app/dm/[address]/metadata.ts +++ b/packages/framekit/src/app/dm/[address]/metadata.ts @@ -16,7 +16,7 @@ async function getParams(params: Promise<{ address: string }>) { export async function generateMetadata({ params }: Props): Promise { const resolvedParams = await params; const userInfo = await getUserInfo(resolvedParams?.address as string); - const imageUrl = `${process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"}/api/dm?address=${userInfo?.address}`; + const imageUrl = `${process.env.NEXT_PUBLIC_URL ?? "http://localhost:3000"}/api/dmimage?address=${userInfo?.address}`; console.log("imageUrl", imageUrl); return { title: `Chat with ${userInfo?.preferredName || resolvedParams?.address}`, From a5bff9d4a3a394ac715b3b2b768a8ae9dee15aaf Mon Sep 17 00:00:00 2001 From: fabri Date: Tue, 17 Dec 2024 12:59:05 -0300 Subject: [PATCH 28/31] params --- packages/framekit/package.json | 1 + packages/framekit/src/app/api/og/route.ts | 60 +++++ .../framekit/src/app/api/receipt/route.tsx | 1 - packages/framekit/src/app/wallet/page.tsx | 142 +++++------- .../framekit/src/components/Chat.module.css | 68 ++++-- packages/framekit/src/components/Chat.tsx | 212 +++++++++++++----- .../framekit/src/components/UrlPreview.tsx | 63 ++++++ yarn.lock | 147 +++++++++++- 8 files changed, 527 insertions(+), 167 deletions(-) create mode 100644 packages/framekit/src/app/api/og/route.ts create mode 100644 packages/framekit/src/components/UrlPreview.tsx diff --git a/packages/framekit/package.json b/packages/framekit/package.json index 0da94f6f6..52b772e25 100644 --- a/packages/framekit/package.json +++ b/packages/framekit/package.json @@ -14,6 +14,7 @@ "ethers": "^6.13.4", "geist": "^1.3.1", "next": "^15.0.3", + "open-graph-scraper": "^6.8.3", "qrcode": "^1.5.4", "qrcode.react": "^4.1.0", "react": "^18.2.0", diff --git a/packages/framekit/src/app/api/og/route.ts b/packages/framekit/src/app/api/og/route.ts new file mode 100644 index 000000000..dbf2af082 --- /dev/null +++ b/packages/framekit/src/app/api/og/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from "next/server"; +import { headers } from "next/headers"; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const url = searchParams.get("url"); + + console.log("Fetching OG data for URL:", url); + + if (!url) { + return NextResponse.json( + { error: "URL parameter is required" }, + { status: 400 }, + ); + } + + // Fetch OpenGraph data from the target URL + const response = await fetch(url, { + headers: { + "User-Agent": "bot", // Some sites require a user agent + }, + }); + + if (!response.ok) { + throw new Error(`Target URL returned status: ${response.status}`); + } + + const html = await response.text(); + + // More robust OG tag extraction + const getMetaContent = (html: string, property: string) => { + const match = html.match( + new RegExp(`]*(?:property|name)=["']${property}["'][^>]*content=["']([^"']*?)["']`, "i") + ); + return match?.[1] || ""; + }; + + const ogData = { + title: getMetaContent(html, "og:title") || html.match(/(.*?)<\/title>/i)?.[1] || "", + description: getMetaContent(html, "og:description") || getMetaContent(html, "description") || "", + image: getMetaContent(html, "og:image") || "", + url, + }; + + console.log("Extracted OG data:", ogData); + + return NextResponse.json(ogData); + } catch (error) { + console.error("Error in OG route:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "Failed to fetch OG data" }, + { status: 500 }, + ); + } +} + +// Configure CORS +export const runtime = "edge"; +export const dynamic = "force-dynamic"; diff --git a/packages/framekit/src/app/api/receipt/route.tsx b/packages/framekit/src/app/api/receipt/route.tsx index 3ca36c135..b50bf2037 100644 --- a/packages/framekit/src/app/api/receipt/route.tsx +++ b/packages/framekit/src/app/api/receipt/route.tsx @@ -107,7 +107,6 @@ export async function GET(req: NextRequest) { <div style={{ fontSize: "20px" }}>{networkName}</div> </div> <div style={{ fontSize: "48px", display: "flex" }}> - You just received{" "} <div style={{ fontFamily: "Inter-SemiBold", diff --git a/packages/framekit/src/app/wallet/page.tsx b/packages/framekit/src/app/wallet/page.tsx index a9c36f38f..114ae125d 100644 --- a/packages/framekit/src/app/wallet/page.tsx +++ b/packages/framekit/src/app/wallet/page.tsx @@ -1,14 +1,17 @@ -import Head from "next/head"; +import { Metadata } from "next"; import PaymentFrame from "../../components/PaymentFrame"; import { extractFrameChain } from "../utils/networks"; -export default async function Home({ - searchParams, -}: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { +type SearchParams = { [key: string]: string | string[] | undefined }; + +type Props = { + searchParams: Promise<SearchParams>; +}; + +// Helper function to get params +async function getParams(searchParams: Promise<SearchParams>) { const resolvedSearchParams = await searchParams; - const params = { + return { url: process.env.NEXT_PUBLIC_URL, agentAddress: (resolvedSearchParams?.agentAddress as string) || @@ -30,89 +33,58 @@ export default async function Home({ baseScanUrl: "https://basescan.org/address/" + resolvedSearchParams?.address, }; +} + +export async function generateMetadata({ + searchParams, +}: Props): Promise<Metadata> { + const params = await getParams(searchParams); const { chainId, tokenAddress } = extractFrameChain(params.networkId); const ethereumUrl = `ethereum:${tokenAddress}@${chainId}/transfer?address=${params.agentAddress}`; const image = `${params.url}/api/wallet?networkId=${params.networkId}&agentAddress=${params.agentAddress}&ownerAddress=${params.ownerAddress}&balance=${params.balance}`; - return ( - <> - <Head> - <meta charSet="utf-8" /> - <meta property="og:title" content="Wallet Information" /> - <meta property="fc:frame" content="vNext" /> - <meta property="of:version" content="vNext" /> - <meta property="of:accepts:xmtp" content="vNext" /> - <meta property="of:image" content={image} /> - <meta property="og:image" content={image} /> - <meta property="fc:frame:image" content={image} /> - <meta property="fc:frame:ratio" content="1.91:1" /> - - <meta property="fc:frame:button:1" content=" Base Scan" /> - <meta property="fc:frame:button:1:action" content="link" /> - <meta - property="fc:frame:button:1:target" - content={params.baseScanUrl} - /> - <meta property="fc:frame:button:2" content="Add funds" /> - <meta property="fc:frame:button:2:action" content="link" /> - <meta property="fc:frame:button:2:target" content={ethereumUrl} /> - - <style> - {` - :root { - --background: #ffffff; - --foreground: #000000; - --accent: #fa6977; - } - - @media (prefers-color-scheme: dark) { - :root { - --background: #ffffff; - --foreground: #000000; - } - } - - * { - margin: 0; - padding: 0; - box-sizing: border-box; - } - - html, body { - background-color: var(--background) !important; - height: 100%; - width: 100%; - } + return { + title: "Wallet Information", + other: { + "fc:frame": "vNext", + "of:version": "vNext", + "of:accepts:xmtp": "vNext", + "of:image": image, + "og:image": image, + "fc:frame:image": image, + "fc:frame:ratio": "1.91:1", + "fc:frame:button:1": "Base Scan", + "fc:frame:button:1:action": "link", + "fc:frame:button:1:target": params.baseScanUrl, + "fc:frame:button:2": "Add funds", + "fc:frame:button:2:action": "link", + "fc:frame:button:2:target": ethereumUrl, + }, + }; +} - body { - display: inline-block; - } +export default async function Home({ searchParams }: Props) { + const params = await getParams(searchParams); + const { chainId, tokenAddress } = extractFrameChain(params.networkId); + const ethereumUrl = `ethereum:${tokenAddress}@${chainId}/transfer?address=${params.agentAddress}`; + const image = `${params.url}/api/wallet?networkId=${params.networkId}&agentAddress=${params.agentAddress}&ownerAddress=${params.ownerAddress}&balance=${params.balance}`; - .form-container { - background-color: var(--background); - border-radius: 0.5rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); - padding: 1.5rem; - } - `} - </style> - </Head> - <div - style={{ - margin: 0, - padding: 0, - backgroundColor: "white", - height: "100%", - display: "inline-block", - width: "100%", - }}> - <PaymentFrame - params={params} - image={image} - url={params.baseScanUrl} - label="View on Base Scan" - /> - </div> - </> + return ( + <div + style={{ + margin: 0, + padding: 0, + backgroundColor: "white", + height: "100%", + display: "inline-block", + width: "100%", + }}> + <PaymentFrame + params={params} + image={image} + url={params.baseScanUrl} + label="View on Base Scan" + /> + </div> ); } diff --git a/packages/framekit/src/components/Chat.module.css b/packages/framekit/src/components/Chat.module.css index 529d1bce5..db1155dbe 100644 --- a/packages/framekit/src/components/Chat.module.css +++ b/packages/framekit/src/components/Chat.module.css @@ -173,27 +173,65 @@ } } +.urlContainer { + margin: 8px 0; + display: flex; + flex-direction: column; + gap: 8px; + max-width: 300px; +} + +.urlPreview { + border-radius: 8px 8px 0 0; + overflow: hidden; + background: #2a2a2a; + border: 1px solid #3a3a3a; + width: 100%; +} + +.previewImage { + width: 100%; + height: 150px; + object-fit: cover; +} + +.buttonContainer { + display: flex; + gap: 1px; + width: 100%; + background: #3a3a3a; +} + .urlButton { - background-color: #3b82f6; - color: white; - padding: 4px 12px; + flex: 1; + padding: 12px; border: none; - border-radius: 4px; + background-color: #0052ff; + color: #f0f0f0; cursor: pointer; - font-size: 13px; - transition: background-color 0.2s; - margin: 0 4px; - vertical-align: middle; - line-height: 1.2; - display: inline-flex; - align-items: center; + font-size: 14px; + transition: all 0.2s; + width: 100%; + text-align: center; } .urlButton:hover { - background-color: #2563eb; + background-color: #3a3a3a; } -.urlButton:disabled { - background-color: #9ca3af; - cursor: not-allowed; +.messageLink { + color: #fa6977; + text-decoration: underline; + word-break: break-all; + width: 100%; + display: block; +} + +.loadingContainer { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + color: #666; + font-size: 0.9rem; } diff --git a/packages/framekit/src/components/Chat.tsx b/packages/framekit/src/components/Chat.tsx index 338a52747..6f7ae50a2 100644 --- a/packages/framekit/src/components/Chat.tsx +++ b/packages/framekit/src/components/Chat.tsx @@ -4,16 +4,36 @@ import { Client as V2Client } from "@xmtp/xmtp-js"; import { Wallet } from "ethers"; import styles from "./Chat.module.css"; import { UserInfo } from "@/app/utils/resolver"; -import { isAddress, parseUnits } from "viem"; +import { http, isAddress, parseUnits } from "viem"; import { extractFrameChain } from "@/app/utils/networks"; import sdk from "@farcaster/frame-sdk"; +import { UrlPreview } from "./UrlPreview"; interface Message { id: string; content: string; sender: string; + timestamp: number; } +type UrlType = "receipt" | "payment" | "wallet" | "unknown"; + +const getUrlType = (url: string): UrlType => { + if (url.includes("/receipt")) return "receipt"; + if (url.includes("/payment")) return "payment"; + if (url.includes("/wallet")) return "wallet"; + return "unknown"; +}; + +const isFrame = async () => { + try { + const context = await sdk.context; + return !!context; // If we can get context, we're in a frame + } catch { + return false; // If we can't get context, we're not in a frame + } +}; + function Chat({ user }: { user: UserInfo }) { const [messages, setMessages] = useState<Message[]>([]); const [newMessage, setNewMessage] = useState(""); @@ -31,6 +51,7 @@ function Chat({ user }: { user: UserInfo }) { const init = async () => { const newWallet = Wallet.createRandom(); + setWallet(newWallet); try { @@ -63,14 +84,18 @@ function Chat({ user }: { user: UserInfo }) { setConversation(conv); // Load existing messages - const messages = await conv.messages(); - console.log("Initial messages loaded:", messages.length); + const existingMessages = await conv.messages(); + console.log("Initial messages loaded:", existingMessages.length); - const formattedMessages = messages.map((msg: any) => ({ - id: msg.id, - content: msg.content, - sender: msg.senderAddress === wallet.address ? "Human" : "Agent", - })); + // Process messages in chronological order + const formattedMessages = existingMessages + .sort((a: any, b: any) => a.sent.getTime() - b.sent.getTime()) + .map((msg: any) => ({ + id: msg.id, + content: msg.content, + sender: msg.senderAddress === wallet.address ? "Human" : "Agent", + timestamp: msg.sent.getTime(), + })); // Add message IDs to processed set formattedMessages.forEach((msg: any) => @@ -79,6 +104,9 @@ function Chat({ user }: { user: UserInfo }) { setMessages(formattedMessages); setIsLoading(false); + + // Start streaming new messages + streamMessages(conv); } catch (error) { console.error("Error initializing conversation:", error); setIsLoading(false); @@ -89,47 +117,49 @@ function Chat({ user }: { user: UserInfo }) { }, [xmtp, recipientInfo, wallet]); const ethereumURL = (url: string) => { - //frames.message-kit.org/payment?networkId=base&amount=0.01&token=USDC&recipientAddress=0x5d8407cb37f12b8c2a7fbb81d182eafa784022ed - - const urlParams = new URLSearchParams(url.split("?")[1]); - const networkId = urlParams.get("networkId"); - const { chainId, tokenAddress } = extractFrameChain(networkId as string); - const amount = urlParams.get("amount"); - const recipientAddress = urlParams.get("recipientAddress"); + try { + const urlObject = new URL(url); + const urlParams = new URLSearchParams(urlObject.search); + const networkId = urlParams.get("networkId"); + const { chainId, tokenAddress } = extractFrameChain(networkId as string); + const amount = urlParams.get("amount"); + const recipientAddress = urlParams.get("recipientAddress"); - const amountUint256 = parseUnits(amount as string, 6); - const ethereumUrl = `ethereum:${tokenAddress}@${chainId}/transfer?address=${recipientAddress}&uint256=${amountUint256}`; + if (!amount || !recipientAddress) { + console.error("Missing required parameters for ethereum URL"); + return url; + } - return ethereumUrl; + const amountUint256 = parseUnits(amount, 6); + return `ethereum:${tokenAddress}@${chainId}/transfer?address=${recipientAddress}&uint256=${amountUint256}`; + } catch (error) { + console.error("Error constructing ethereum URL:", error); + return url; + } }; - useEffect(() => { - const streamMessages = async () => { - if (!conversation) return; - try { - // Stream new messages - for await (const message of await conversation.streamMessages()) { - console.log("Received message:", message.id, message.content); - if (!processedMessageIds.has(message.id)) { - processedMessageIds.add(message.id); - setMessages((prevMessages) => [ - ...prevMessages, - { - id: message.id, - content: message.content, - sender: - message.senderAddress === wallet.address ? "Human" : "Agent", - }, - ]); - } + const streamMessages = async (conv: any) => { + try { + for await (const message of await conv.streamMessages()) { + console.log("Received message:", message.id, message.content); + if (!processedMessageIds.has(message.id)) { + processedMessageIds.add(message.id); + setMessages((prevMessages) => [ + ...prevMessages, + { + id: message.id, + content: message.content, + sender: + message.senderAddress === wallet.address ? "Human" : "Agent", + timestamp: message.sent.getTime(), + }, + ]); } - } catch (error) { - console.error("Error streaming messages:", error); } - }; - - streamMessages(); - }, [conversation, wallet]); + } catch (error) { + console.error("Error streaming messages:", error); + } + }; const initXmtp = async (wallet: any) => { try { @@ -166,6 +196,7 @@ function Chat({ user }: { user: UserInfo }) { id: sentMessage.id, content: newMessage, sender: "Human", + timestamp: new Date().getTime(), }, ]); @@ -174,29 +205,81 @@ function Chat({ user }: { user: UserInfo }) { console.error("Error sending message:", error); } }; - const openUrl = useCallback( - (url: string) => { - sdk.actions.openUrl(url); - }, - [newMessage], - ); + const openUrl = useCallback(async (url: string) => { + try { + const inFrame = await isFrame(); + if (inFrame) { + sdk.actions.openUrl(url); + } else { + window.open(url, "_blank"); + } + } catch (error) { + console.error("Error opening URL:", error); + // Fallback to traditional navigation if something goes wrong + window.location.href = url; + } + }, []); const renderMessageContent = (content: string) => { const urlRegex = /(https?:\/\/[^\s]+)/g; - return content.split(urlRegex).map((part, index) => { + const parts = content.split(urlRegex); + + return parts.map((part, index) => { if (urlRegex.test(part)) { - return ( - <button - key={index} - onClick={() => { - window.location.href = ethereumURL(part); - const ethUrl = ethereumURL(part); - openUrl(ethUrl); - }} - className={`${styles.urlButton}`}> - Pay in USDC - </button> - ); + try { + const urlType = getUrlType(part); + const isMessageKitUrl = part.includes("frames.message-kit.org"); + + return ( + <div key={index} className={styles.urlContainer}> + {isMessageKitUrl && <UrlPreview url={part} urlType={urlType} />} + <div className={styles.buttonContainer}> + {urlType === "payment" && ( + <button + onClick={() => { + const ethUrl = ethereumURL(part); + openUrl(ethUrl); + }} + className={styles.urlButton}> + Pay in USDC + </button> + )} + {urlType === "receipt" && ( + <button + onClick={() => { + console.log("Viewing receipt:", part); + openUrl(part); + }} + className={styles.urlButton}> + View Receipt + </button> + )} + {urlType === "wallet" && ( + <button + onClick={() => { + console.log("Viewing wallet:", part); + openUrl(part); + }} + className={styles.urlButton}> + View Wallet + </button> + )} + {urlType === "unknown" && ( + <a + href={part} + target="_blank" + rel="noopener noreferrer" + className={styles.messageLink}> + {part} + </a> + )} + </div> + </div> + ); + } catch (error) { + console.error("Error rendering URL content:", error); + return part; + } } return part; }); @@ -218,6 +301,11 @@ function Chat({ user }: { user: UserInfo }) { : user?.address)} </div> </div> + {isLoading && ( + <div className={styles.loadingContainer}> + <span>Loading messages...</span> + </div> + )} <div className={styles.messagesContainer}> {messages.map((msg, index) => ( <div key={msg.id || index} className={styles.message}> diff --git a/packages/framekit/src/components/UrlPreview.tsx b/packages/framekit/src/components/UrlPreview.tsx new file mode 100644 index 000000000..cbf25c9c4 --- /dev/null +++ b/packages/framekit/src/components/UrlPreview.tsx @@ -0,0 +1,63 @@ +import { useState, useEffect } from "react"; +import styles from "./Chat.module.css"; + +interface OgData { + image?: string; + title?: string; + description?: string; +} + +export function UrlPreview({ url, urlType }: { url: string; urlType: string }) { + const [ogData, setOgData] = useState<OgData | null>(null); + const [loading, setLoading] = useState(true); + + const fetchOgData = async (url: string) => { + try { + console.log("Fetching OG data for URL:", url); + const response = await fetch(`/api/og?url=${encodeURIComponent(url)}`); + console.log("OG Response status:", response.status); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log("OG Data received:", data); + return data; + } catch (error) { + console.error("Error fetching OG data:", error); + return null; + } + }; + + useEffect(() => { + if (url) { + setLoading(true); + fetchOgData(url).then((data) => { + if (data) { + setOgData(data); + } + setLoading(false); + }); + } + }, [url]); + + // Don't render anything while loading or if no image is available + if (loading) { + return <div className={styles.loading}>Loading preview...</div>; + } + + if (!ogData?.image) { + return null; + } + + return ( + <div className={styles.urlPreview}> + <img + src={ogData.image} + alt={ogData.title || "Preview"} + className={styles.previewImage} + /> + </div> + ); +} diff --git a/yarn.lock b/yarn.lock index f6040081c..02904dcc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4504,6 +4504,13 @@ __metadata: languageName: node linkType: hard +"chardet@npm:^2.0.0": + version: 2.0.0 + resolution: "chardet@npm:2.0.0" + checksum: 10/ada7a51c0b6f53dad0dcf3105fcf9cb613cf70a7984a1e0e69cc01b7c456ae3d747d1ed0fc563731bdfea5c3a7a16969f02897c8b88b4d2a569e73b9a4c96869 + languageName: node + linkType: hard + "check-error@npm:^2.1.1": version: 2.1.1 resolution: "check-error@npm:2.1.1" @@ -4511,6 +4518,39 @@ __metadata: languageName: node linkType: hard +"cheerio-select@npm:^2.1.0": + version: 2.1.0 + resolution: "cheerio-select@npm:2.1.0" + dependencies: + boolbase: "npm:^1.0.0" + css-select: "npm:^5.1.0" + css-what: "npm:^6.1.0" + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.0.1" + checksum: 10/b5d89208c23468c3a32d1e04f88b9e8c6e332e3649650c5cd29255e2cebc215071ae18563f58c3dc3f6ef4c234488fc486035490fceb78755572288245e2931a + languageName: node + linkType: hard + +"cheerio@npm:^1.0.0-rc.12": + version: 1.0.0 + resolution: "cheerio@npm:1.0.0" + dependencies: + cheerio-select: "npm:^2.1.0" + dom-serializer: "npm:^2.0.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.1.0" + encoding-sniffer: "npm:^0.2.0" + htmlparser2: "npm:^9.1.0" + parse5: "npm:^7.1.2" + parse5-htmlparser2-tree-adapter: "npm:^7.0.0" + parse5-parser-stream: "npm:^7.1.2" + undici: "npm:^6.19.5" + whatwg-mimetype: "npm:^4.0.0" + checksum: 10/b535070add0f86b0a1f234274ad3ffb2c1c375c05b322d8057e89c3c797b3b4d2f05826c34a04df218bec9abf21b9f0d0bd71974a8dfe28b943fb87ab0170c38 + languageName: node + linkType: hard + "chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" @@ -4892,6 +4932,19 @@ __metadata: languageName: node linkType: hard +"css-select@npm:^5.1.0": + version: 5.1.0 + resolution: "css-select@npm:5.1.0" + dependencies: + boolbase: "npm:^1.0.0" + css-what: "npm:^6.1.0" + domhandler: "npm:^5.0.2" + domutils: "npm:^3.0.1" + nth-check: "npm:^2.0.1" + checksum: 10/d486b1e7eb140468218a5ab5af53257e01f937d2173ac46981f6b7de9c5283d55427a36715dc8decfc0c079cf89259ac5b41ef58f6e1a422eee44ab8bfdc78da + languageName: node + linkType: hard + "css-selector-parser@npm:^3.0.0": version: 3.0.5 resolution: "css-selector-parser@npm:3.0.5" @@ -5202,7 +5255,7 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^3.0.1": +"domutils@npm:^3.0.1, domutils@npm:^3.1.0": version: 3.1.0 resolution: "domutils@npm:3.1.0" dependencies: @@ -5338,6 +5391,16 @@ __metadata: languageName: node linkType: hard +"encoding-sniffer@npm:^0.2.0": + version: 0.2.0 + resolution: "encoding-sniffer@npm:0.2.0" + dependencies: + iconv-lite: "npm:^0.6.3" + whatwg-encoding: "npm:^3.1.1" + checksum: 10/fe61a759dbef4d94ddc6f4fa645459897f4275eba04f0135d0459099b5f62fbba8a7ae57d23c9ec9b118c4c39ce056b51f1b8e62ad73a8ab365699448d655f4c + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -5367,7 +5430,7 @@ __metadata: languageName: unknown linkType: soft -"entities@npm:^4.2.0, entities@npm:^4.4.0": +"entities@npm:^4.2.0, entities@npm:^4.4.0, entities@npm:^4.5.0": version: 4.5.0 resolution: "entities@npm:4.5.0" checksum: 10/ede2a35c9bce1aeccd055a1b445d41c75a14a2bb1cd22e242f20cf04d236cdcd7f9c859eb83f76885327bfae0c25bf03303665ee1ce3d47c5927b98b0e3e3d48 @@ -6156,6 +6219,7 @@ __metadata: ethers: "npm:^6.13.4" geist: "npm:^1.3.1" next: "npm:^15.0.3" + open-graph-scraper: "npm:^6.8.3" qrcode: "npm:^1.5.4" qrcode.react: "npm:^4.1.0" react: "npm:^18.2.0" @@ -6716,6 +6780,18 @@ __metadata: languageName: node linkType: hard +"htmlparser2@npm:^9.1.0": + version: 9.1.0 + resolution: "htmlparser2@npm:9.1.0" + dependencies: + domelementtype: "npm:^2.3.0" + domhandler: "npm:^5.0.3" + domutils: "npm:^3.1.0" + entities: "npm:^4.5.0" + checksum: 10/6352fa2a5495781fa9a02c9049908334cd068ff36d753870d30cd13b841e99c19646717567a2f9e9c44075bbe43d364e102f9d013a731ce962226d63746b794f + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -6788,7 +6864,7 @@ __metadata: languageName: node linkType: hard -"iconv-lite@npm:^0.6.2": +"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" dependencies: @@ -8733,7 +8809,7 @@ __metadata: languageName: node linkType: hard -"nth-check@npm:^2.0.0": +"nth-check@npm:^2.0.0, nth-check@npm:^2.0.1": version: 2.1.1 resolution: "nth-check@npm:2.1.1" dependencies: @@ -8806,6 +8882,18 @@ __metadata: languageName: node linkType: hard +"open-graph-scraper@npm:^6.8.3": + version: 6.8.3 + resolution: "open-graph-scraper@npm:6.8.3" + dependencies: + chardet: "npm:^2.0.0" + cheerio: "npm:^1.0.0-rc.12" + iconv-lite: "npm:^0.6.3" + undici: "npm:^6.21.0" + checksum: 10/db5ca0996a22c2c288d120a7767d81325494d482bbe1cbb12ae6c2e9f443cfa1f5368fa03bd01ccab8cf16665e850ddc4d08594b86f64c90eae9788a753d5fbc + languageName: node + linkType: hard + "openai@npm:^4.52.7": version: 4.76.3 resolution: "openai@npm:4.76.3" @@ -9034,6 +9122,34 @@ __metadata: languageName: node linkType: hard +"parse5-htmlparser2-tree-adapter@npm:^7.0.0": + version: 7.1.0 + resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0" + dependencies: + domhandler: "npm:^5.0.3" + parse5: "npm:^7.0.0" + checksum: 10/75910af9137451e9c53e1e0d712f7393f484e89e592b1809ee62ad6cedd61b98daeaa5206ff5d9f06778002c91fac311afedde4880e1916fdb44fa71199dae73 + languageName: node + linkType: hard + +"parse5-parser-stream@npm:^7.1.2": + version: 7.1.2 + resolution: "parse5-parser-stream@npm:7.1.2" + dependencies: + parse5: "npm:^7.0.0" + checksum: 10/75b232d460bce6bd0e35012750a78ef034f40ccf550b7c6cec3122395af6b4553202ad3663ad468cf537ead5a2e13b6727670395fd0ff548faccad1dc2dc93cf + languageName: node + linkType: hard + +"parse5@npm:^7.0.0, parse5@npm:^7.1.2": + version: 7.2.1 + resolution: "parse5@npm:7.2.1" + dependencies: + entities: "npm:^4.5.0" + checksum: 10/fd1a8ad1540d871e1ad6ca9bf5b67e30280886f1ce4a28052c0cb885723aa984d8cb1ec3da998349a6146960c8a84aa87b1a42600eb3b94495c7303476f2f88e + languageName: node + linkType: hard + "parseley@npm:^0.12.0": version: 0.12.1 resolution: "parseley@npm:0.12.1" @@ -11446,6 +11562,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^6.19.5, undici@npm:^6.21.0": + version: 6.21.0 + resolution: "undici@npm:6.21.0" + checksum: 10/c8ff80dcadfcf613e7fe697c37519fca070fcf1cfccc69ffb6a7080a22e225eb79d232e9f70e32b099b3e67ac4216e8fd615e188cfb792e09df9233471ec17e0 + languageName: node + linkType: hard + "unified@npm:^10.1.2": version: 10.1.2 resolution: "unified@npm:10.1.2" @@ -11988,6 +12111,22 @@ __metadata: languageName: node linkType: hard +"whatwg-encoding@npm:^3.1.1": + version: 3.1.1 + resolution: "whatwg-encoding@npm:3.1.1" + dependencies: + iconv-lite: "npm:0.6.3" + checksum: 10/bbef815eb67f91487c7f2ef96329743f5fd8357d7d62b1119237d25d41c7e452dff8197235b2d3c031365a17f61d3bb73ca49d0ed1582475aa4a670815e79534 + languageName: node + linkType: hard + +"whatwg-mimetype@npm:^4.0.0": + version: 4.0.0 + resolution: "whatwg-mimetype@npm:4.0.0" + checksum: 10/894a618e2d90bf444b6f309f3ceb6e58cf21b2beaa00c8b333696958c4076f0c7b30b9d33413c9ffff7c5832a0a0c8569e5bb347ef44beded72aeefd0acd62e8 + languageName: node + linkType: hard + "whatwg-url@npm:^5.0.0": version: 5.0.0 resolution: "whatwg-url@npm:5.0.0" From a87bbb90477934f1adf470c003139f46b955d741 Mon Sep 17 00:00:00 2001 From: fabri <fguespe@gmail.com> Date: Tue, 17 Dec 2024 13:29:50 -0300 Subject: [PATCH 29/31] push --- packages/framekit/package.json | 1 + .../framekit/src/app/api/dmimage/route.tsx | 1 - packages/framekit/src/app/api/og/route.ts | 46 ++++++++++----- packages/framekit/src/app/payment/page.tsx | 9 ++- packages/framekit/src/components/Chat.tsx | 1 - .../framekit/src/components/UrlPreview.tsx | 59 ++++++++++--------- 6 files changed, 71 insertions(+), 46 deletions(-) diff --git a/packages/framekit/package.json b/packages/framekit/package.json index 52b772e25..1f4b17c7e 100644 --- a/packages/framekit/package.json +++ b/packages/framekit/package.json @@ -11,6 +11,7 @@ "dependencies": { "@farcaster/frame-sdk": "0.0.10", "@types/next": "^9.0.0", + "cheerio": "^1.0.0", "ethers": "^6.13.4", "geist": "^1.3.1", "next": "^15.0.3", diff --git a/packages/framekit/src/app/api/dmimage/route.tsx b/packages/framekit/src/app/api/dmimage/route.tsx index 1e06a300c..111de3446 100644 --- a/packages/framekit/src/app/api/dmimage/route.tsx +++ b/packages/framekit/src/app/api/dmimage/route.tsx @@ -25,7 +25,6 @@ export async function GET(req: NextRequest) { const address = req.nextUrl.searchParams.get("address") ?? ""; const user = await getUserInfo(address); - console.log("Resolved user info:", user); const params = { url: process.env.NEXT_PUBLIC_URL || "http://localhost:3000", diff --git a/packages/framekit/src/app/api/og/route.ts b/packages/framekit/src/app/api/og/route.ts index dbf2af082..f2f95ac8f 100644 --- a/packages/framekit/src/app/api/og/route.ts +++ b/packages/framekit/src/app/api/og/route.ts @@ -1,13 +1,11 @@ import { NextResponse } from "next/server"; -import { headers } from "next/headers"; +import * as cheerio from 'cheerio'; export async function GET(request: Request) { try { const { searchParams } = new URL(request.url); const url = searchParams.get("url"); - console.log("Fetching OG data for URL:", url); - if (!url) { return NextResponse.json( { error: "URL parameter is required" }, @@ -15,32 +13,46 @@ export async function GET(request: Request) { ); } - // Fetch OpenGraph data from the target URL + console.log("Fetching OG data for URL:", url); + const response = await fetch(url, { headers: { - "User-Agent": "bot", // Some sites require a user agent + "user-agent": "bot", }, }); if (!response.ok) { - throw new Error(`Target URL returned status: ${response.status}`); + throw new Error(`Failed to fetch: ${response.status}`); } const html = await response.text(); + const $ = cheerio.load(html); - // More robust OG tag extraction - const getMetaContent = (html: string, property: string) => { - const match = html.match( - new RegExp(`<meta[^>]*(?:property|name)=["']${property}["'][^>]*content=["']([^"']*?)["']`, "i") + // Extract meta tags using cheerio + const getMetaContent = (property: string) => { + return ( + $(`meta[property="${property}"]`).attr('content') || + $(`meta[name="${property}"]`).attr('content') ); - return match?.[1] || ""; }; const ogData = { - title: getMetaContent(html, "og:title") || html.match(/<title>(.*?)<\/title>/i)?.[1] || "", - description: getMetaContent(html, "og:description") || getMetaContent(html, "description") || "", - image: getMetaContent(html, "og:image") || "", + title: + getMetaContent("og:title") || + getMetaContent("twitter:title") || + $('title').text() || + url, + description: + getMetaContent("og:description") || + getMetaContent("twitter:description") || + getMetaContent("description") || + "", + image: + getMetaContent("og:image") || + getMetaContent("twitter:image") || + "", url, + siteName: getMetaContent("og:site_name") || "", }; console.log("Extracted OG data:", ogData); @@ -49,12 +61,14 @@ export async function GET(request: Request) { } catch (error) { console.error("Error in OG route:", error); return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to fetch OG data" }, + { + error: + error instanceof Error ? error.message : "Failed to fetch OG data", + }, { status: 500 }, ); } } -// Configure CORS export const runtime = "edge"; export const dynamic = "force-dynamic"; diff --git a/packages/framekit/src/app/payment/page.tsx b/packages/framekit/src/app/payment/page.tsx index b7724a00a..843a1fa59 100644 --- a/packages/framekit/src/app/payment/page.tsx +++ b/packages/framekit/src/app/payment/page.tsx @@ -35,7 +35,14 @@ export async function generateMetadata({ const { chainId, tokenAddress } = extractFrameChain(params.networkId); const amountUint256 = parseUnits(params.amount.toString(), 6); const ethereumUrl = `ethereum:${tokenAddress}@${chainId}/transfer?address=${params.recipientAddress}&uint256=${amountUint256}`; - const image = `${params.url}/api/payment?networkId=${params.networkId}&amount=${params.amount}&recipientAddress=${params.recipientAddress}`; + + // Fix: Properly construct the image URL without HTML entities + const imageParams = new URLSearchParams({ + networkId: params.networkId, + amount: params.amount.toString(), + recipientAddress: params.recipientAddress, + }); + const image = `${params.url}/api/payment?${imageParams.toString()}`; return { title: "Ethereum Payment", diff --git a/packages/framekit/src/components/Chat.tsx b/packages/framekit/src/components/Chat.tsx index 6f7ae50a2..969e2a976 100644 --- a/packages/framekit/src/components/Chat.tsx +++ b/packages/framekit/src/components/Chat.tsx @@ -56,7 +56,6 @@ function Chat({ user }: { user: UserInfo }) { try { setRecipientInfo(user); - console.log("User info:", user); if (user?.address) { console.log("Initializing XMTP with address:", user.address); await initXmtp(newWallet); diff --git a/packages/framekit/src/components/UrlPreview.tsx b/packages/framekit/src/components/UrlPreview.tsx index cbf25c9c4..c9d98d99d 100644 --- a/packages/framekit/src/components/UrlPreview.tsx +++ b/packages/framekit/src/components/UrlPreview.tsx @@ -5,44 +5,49 @@ interface OgData { image?: string; title?: string; description?: string; + url?: string; } -export function UrlPreview({ url, urlType }: { url: string; urlType: string }) { +export const UrlPreview = ({ + url, + urlType, +}: { + url: string; + urlType?: string; +}) => { const [ogData, setOgData] = useState<OgData | null>(null); const [loading, setLoading] = useState(true); - const fetchOgData = async (url: string) => { - try { - console.log("Fetching OG data for URL:", url); - const response = await fetch(`/api/og?url=${encodeURIComponent(url)}`); - console.log("OG Response status:", response.status); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + useEffect(() => { + const fetchOGData = async () => { + try { + const decodedUrl = decodeURIComponent(url); + console.log("Fetching OG data for URL:", decodedUrl); - const data = await response.json(); - console.log("OG Data received:", data); - return data; - } catch (error) { - console.error("Error fetching OG data:", error); - return null; - } - }; + // Use the /api/og endpoint to avoid CORS + const response = await fetch( + `/api/og?url=${encodeURIComponent(decodedUrl)}`, + ); - useEffect(() => { - if (url) { - setLoading(true); - fetchOgData(url).then((data) => { - if (data) { - setOgData(data); + if (!response.ok) { + throw new Error(`Failed to fetch OG data: ${response.status}`); } + + const data = await response.json(); + console.log("Extracted OG data:", data); + setOgData(data); + } catch (error) { + console.error("Error fetching OG data:", error); + } finally { setLoading(false); - }); + } + }; + + if (url) { + fetchOGData(); } }, [url]); - // Don't render anything while loading or if no image is available if (loading) { return <div className={styles.loading}>Loading preview...</div>; } @@ -60,4 +65,4 @@ export function UrlPreview({ url, urlType }: { url: string; urlType: string }) { /> </div> ); -} +}; From 98711c12ca4cc12f11f275bc36820874f2197eea Mon Sep 17 00:00:00 2001 From: fabri <fguespe@gmail.com> Date: Tue, 17 Dec 2024 13:32:23 -0300 Subject: [PATCH 30/31] fix --- packages/framekit/package.json | 1 - packages/message-kit/src/plugins/cdp.ts | 2 ++ yarn.lock | 25 +++---------------------- 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/packages/framekit/package.json b/packages/framekit/package.json index 1f4b17c7e..5ed26882f 100644 --- a/packages/framekit/package.json +++ b/packages/framekit/package.json @@ -15,7 +15,6 @@ "ethers": "^6.13.4", "geist": "^1.3.1", "next": "^15.0.3", - "open-graph-scraper": "^6.8.3", "qrcode": "^1.5.4", "qrcode.react": "^4.1.0", "react": "^18.2.0", diff --git a/packages/message-kit/src/plugins/cdp.ts b/packages/message-kit/src/plugins/cdp.ts index b573b7f03..79d8b6c09 100644 --- a/packages/message-kit/src/plugins/cdp.ts +++ b/packages/message-kit/src/plugins/cdp.ts @@ -171,6 +171,8 @@ export class WalletService implements AgentWallet { toAddress = toAddress.toLowerCase(); let from = await this.getWallet(fromAddress); if (!from) return undefined; + if (!Number(amount)) return undefined; + console.log(`Retrieved wallet data for ${fromAddress}`); let balance = await from.wallet.getBalance(Coinbase.assets.Usdc); if (Number(balance) < amount) { diff --git a/yarn.lock b/yarn.lock index 02904dcc3..d04048703 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4504,13 +4504,6 @@ __metadata: languageName: node linkType: hard -"chardet@npm:^2.0.0": - version: 2.0.0 - resolution: "chardet@npm:2.0.0" - checksum: 10/ada7a51c0b6f53dad0dcf3105fcf9cb613cf70a7984a1e0e69cc01b7c456ae3d747d1ed0fc563731bdfea5c3a7a16969f02897c8b88b4d2a569e73b9a4c96869 - languageName: node - linkType: hard - "check-error@npm:^2.1.1": version: 2.1.1 resolution: "check-error@npm:2.1.1" @@ -4532,7 +4525,7 @@ __metadata: languageName: node linkType: hard -"cheerio@npm:^1.0.0-rc.12": +"cheerio@npm:^1.0.0": version: 1.0.0 resolution: "cheerio@npm:1.0.0" dependencies: @@ -6216,10 +6209,10 @@ __metadata: "@types/node": "npm:^20" "@types/react": "npm:^18" "@types/react-dom": "npm:^18" + cheerio: "npm:^1.0.0" ethers: "npm:^6.13.4" geist: "npm:^1.3.1" next: "npm:^15.0.3" - open-graph-scraper: "npm:^6.8.3" qrcode: "npm:^1.5.4" qrcode.react: "npm:^4.1.0" react: "npm:^18.2.0" @@ -8882,18 +8875,6 @@ __metadata: languageName: node linkType: hard -"open-graph-scraper@npm:^6.8.3": - version: 6.8.3 - resolution: "open-graph-scraper@npm:6.8.3" - dependencies: - chardet: "npm:^2.0.0" - cheerio: "npm:^1.0.0-rc.12" - iconv-lite: "npm:^0.6.3" - undici: "npm:^6.21.0" - checksum: 10/db5ca0996a22c2c288d120a7767d81325494d482bbe1cbb12ae6c2e9f443cfa1f5368fa03bd01ccab8cf16665e850ddc4d08594b86f64c90eae9788a753d5fbc - languageName: node - linkType: hard - "openai@npm:^4.52.7": version: 4.76.3 resolution: "openai@npm:4.76.3" @@ -11562,7 +11543,7 @@ __metadata: languageName: node linkType: hard -"undici@npm:^6.19.5, undici@npm:^6.21.0": +"undici@npm:^6.19.5": version: 6.21.0 resolution: "undici@npm:6.21.0" checksum: 10/c8ff80dcadfcf613e7fe697c37519fca070fcf1cfccc69ffb6a7080a22e225eb79d232e9f70e32b099b3e67ac4216e8fd615e188cfb792e09df9233471ec17e0 From c3d1c9e4ce58a74c89f64af0eecaf2b16675b120 Mon Sep 17 00:00:00 2001 From: fabri <fguespe@gmail.com> Date: Tue, 17 Dec 2024 13:51:37 -0300 Subject: [PATCH 31/31] 1.2.33 --- package.json | 4 ++-- packages/create-message-kit/index.js | 2 +- packages/create-message-kit/package.json | 4 ++-- packages/docs/vocs.config.tsx | 2 +- packages/framekit/src/app/payment/page.tsx | 9 +-------- packages/message-kit/package.json | 4 ++-- packages/message-kit/src/skills/concierge.ts | 14 ++++---------- templates/paymentagent/example_prompt.md | 4 ++-- 8 files changed, 15 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 4b647bbd1..c46158c4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "message-kit-monorepo", - "version": "1.2.32", + "version": "1.2.33", "private": true, "type": "module", "workspaces": [ @@ -56,4 +56,4 @@ "engines": { "node": ">=20" } -} +} \ No newline at end of file diff --git a/packages/create-message-kit/index.js b/packages/create-message-kit/index.js index a87cb366c..281b26724 100755 --- a/packages/create-message-kit/index.js +++ b/packages/create-message-kit/index.js @@ -7,7 +7,7 @@ import { default as fs } from "fs-extra"; import { isCancel } from "@clack/prompts"; import { detect } from "detect-package-manager"; import pc from "picocolors"; -const defVersion = "1.2.32"; +const defVersion = "1.2.33"; const __dirname = dirname(fileURLToPath(import.meta.url)); // Read package.json to get the version diff --git a/packages/create-message-kit/package.json b/packages/create-message-kit/package.json index 6519e9169..f46736a4e 100644 --- a/packages/create-message-kit/package.json +++ b/packages/create-message-kit/package.json @@ -1,6 +1,6 @@ { "name": "create-message-kit", - "version": "1.2.32", + "version": "1.2.33", "license": "MIT", "type": "module", "main": "index.js", @@ -36,4 +36,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} +} \ No newline at end of file diff --git a/packages/docs/vocs.config.tsx b/packages/docs/vocs.config.tsx index 577fc6575..37811d448 100644 --- a/packages/docs/vocs.config.tsx +++ b/packages/docs/vocs.config.tsx @@ -69,7 +69,7 @@ export default defineConfig({ link: "https://github.com/ephemeraHQ/message-kit", }, ], - topNav: [{ text: "v1.2.32", link: "/changelog" }], + topNav: [{ text: "v1.2.33", link: "/changelog" }], editLink: { pattern: "https://github.com/ephemeraHQ/message-kit/blob/main/packages/docs/pages/:path", diff --git a/packages/framekit/src/app/payment/page.tsx b/packages/framekit/src/app/payment/page.tsx index 843a1fa59..b7724a00a 100644 --- a/packages/framekit/src/app/payment/page.tsx +++ b/packages/framekit/src/app/payment/page.tsx @@ -35,14 +35,7 @@ export async function generateMetadata({ const { chainId, tokenAddress } = extractFrameChain(params.networkId); const amountUint256 = parseUnits(params.amount.toString(), 6); const ethereumUrl = `ethereum:${tokenAddress}@${chainId}/transfer?address=${params.recipientAddress}&uint256=${amountUint256}`; - - // Fix: Properly construct the image URL without HTML entities - const imageParams = new URLSearchParams({ - networkId: params.networkId, - amount: params.amount.toString(), - recipientAddress: params.recipientAddress, - }); - const image = `${params.url}/api/payment?${imageParams.toString()}`; + const image = `${params.url}/api/payment?networkId=${params.networkId}&amount=${params.amount}&recipientAddress=${params.recipientAddress}`; return { title: "Ethereum Payment", diff --git a/packages/message-kit/package.json b/packages/message-kit/package.json index b2b1d9e11..af529a816 100644 --- a/packages/message-kit/package.json +++ b/packages/message-kit/package.json @@ -1,6 +1,6 @@ { "name": "@xmtp/message-kit", - "version": "1.2.32", + "version": "1.2.33", "license": "MIT", "type": "module", "exports": { @@ -92,4 +92,4 @@ "access": "public", "registry": "https://registry.npmjs.org/" } -} +} \ No newline at end of file diff --git a/packages/message-kit/src/skills/concierge.ts b/packages/message-kit/src/skills/concierge.ts index eeb4b0d01..b0d729ef0 100644 --- a/packages/message-kit/src/skills/concierge.ts +++ b/packages/message-kit/src/skills/concierge.ts @@ -167,21 +167,15 @@ async function notifyUser( ) { if (transaction) { await context.dm(`Transfer completed successfully`); - if (transaction.getTransactionHash !== undefined) { + if ((await transaction.getTransactionHash()) !== undefined) { const url = await FrameKit.sendReceipt( - `https://basescan.org/tx/${transaction.getTransactionHash()}`, + `https://basescan.org/tx/${await transaction.getTransactionHash()}`, amount, ); await context.dm(url); - } else if (transaction.txHash !== undefined) { + } else if ((await transaction.getTransaction()) !== undefined) { const url = await FrameKit.sendReceipt( - `https://basescan.org/tx/${transaction.txHash}`, - amount, - ); - await context.dm(url); - } else if (transaction.getTransaction !== undefined) { - const url = await FrameKit.sendReceipt( - `https://basescan.org/tx/${transaction.getTransaction()}`, + `https://basescan.org/tx/${await transaction.getTransaction()}`, amount, ); await context.dm(url); diff --git a/templates/paymentagent/example_prompt.md b/templates/paymentagent/example_prompt.md index c5665c0eb..329a7a941 100644 --- a/templates/paymentagent/example_prompt.md +++ b/templates/paymentagent/example_prompt.md @@ -13,14 +13,14 @@ Vibe: A high-energy, risk-embracing personality from the crypto trading world. T - Do not make guesses or assumptions - Only answer if the verified information is in the prompt. - Focus only on helping users with operations detailed below. -- Date: Mon, 16 Dec 2024 21:41:44 GMT, +- Date: Tue, 17 Dec 2024 16:46:28 GMT, ## User context - Start by fetch their domain from or Converse username - Call the user by their name or domain, in case they have one - Ask for a name (if they don't have one) so you can suggest domains. -- Message sent date: 2024-12-16T21:42:17.162Z +- Message sent date: 2024-12-17T16:46:54.680Z - Users address is: 0x40f08f0f853d1c42c61815652b7ccd5a50f0be09 - Users name is: ArizonaOregon - Converse username is: ArizonaOregon