From 9c4901fe1322dfe3d22ce9a7a06b3a8c6c56e65b Mon Sep 17 00:00:00 2001 From: Siddesh Date: Wed, 27 Mar 2024 02:28:34 +0530 Subject: [PATCH 01/16] feat(chat-bubble): adds frames support --- .../chat/ChatViewBubble/ChatViewBubble.tsx | 453 +++++++++++++++++- .../lib/components/chat/reusables/Button.tsx | 31 +- packages/uiweb/src/lib/types/index.ts | 137 ++++-- .../uiweb/src/lib/utilities/extractWebLink.ts | 5 + .../src/lib/utilities/getFormattedMetadata.ts | 106 ++++ .../uiweb/src/lib/utilities/hasWebLink.ts | 5 + packages/uiweb/src/lib/utilities/index.ts | 5 +- 7 files changed, 665 insertions(+), 77 deletions(-) create mode 100644 packages/uiweb/src/lib/utilities/extractWebLink.ts create mode 100644 packages/uiweb/src/lib/utilities/getFormattedMetadata.ts create mode 100644 packages/uiweb/src/lib/utilities/hasWebLink.ts diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index e7a672dbb..f55e1b4de 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -11,20 +11,30 @@ import styled from 'styled-components'; import { TwitterTweetEmbed } from 'react-twitter-embed'; import { Section, Span, Image } from '../../reusables'; +import useToast from '../reusables/NewToast'; import { checkTwitterUrl } from '../helpers/twitter'; import { ChatDataContext } from '../../../context'; -import { useChatData } from '../../../hooks'; +import { useAccount, useChatData } from '../../../hooks'; import { ThemeContext } from '../theme/ThemeProvider'; -import { FileMessageContent } from '../../../types'; +import { FileMessageContent, FrameDetails } from '../../../types'; import { IMessagePayload, TwitterFeedReturnType } from '../exportedTypes'; -import { FILE_ICON } from '../../../config'; +import { allowedNetworks, ENV, FILE_ICON } from '../../../config'; import { formatFileSize, getPfp, pCAIP10ToWallet, shortenText, } from '../../../helpers'; +import { + extractWebLink, + getFormattedMetadata, + hasWebLink, +} from '../../../utilities'; +import { Button, TextInput } from '../reusables'; +import { MdError, MdOpenInNew } from 'react-icons/md'; +import { FaBell, FaLink } from 'react-icons/fa'; +import { BsLightning } from 'react-icons/bs'; const SenderMessageAddress = ({ chat }: { chat: IMessagePayload }) => { const { account } = useContext(ChatDataContext); @@ -108,24 +118,415 @@ const MessageWrapper = ({ {isGroup && } {children} - ); }; +const FrameRenderer = ({ url, account }: { url: string; account: string }) => { + const { env, user } = useChatData(); + const { chainId, switchChain, provider } = useAccount({ env }); + const frameRenderer = useToast(); + const [metaTags, setMetaTags] = useState({ + image: '', + siteURL: '', + postURL: '', + buttons: [], + input: { name: '', content: '' }, + }); + const [inputText, setInputText] = useState(undefined); + // Fetches the metadata for the URL to fetch the Frame + + useEffect(() => { + const fetchMetaTags = async (url: string) => { + try { + const response = await fetch(url, { + method: 'GET', + }); + + const htmlText = await response.text(); + + const frameDetails: FrameDetails = getFormattedMetadata(url, htmlText); + + setMetaTags(frameDetails); + } catch (err) { + console.error('Error fetching meta tags for rendering frame:', err); + } + }; + + if (url) { + fetchMetaTags(url); + } + }, [url]); + const getButtonIconContent = ( + buttonAction: string, + buttonContent: string + ) => { + switch (buttonAction) { + case 'link': + return ( + + {buttonContent} + + ); + + case 'post_redirect': + return ( + + {buttonContent} + + ); + case 'tx': + return ( + + {buttonContent} + + ); + + case buttonAction.includes('subscribe') && 'subscribe': + return ( + + {buttonContent} + + ); + default: + return ( + + {buttonContent} + + ); + } + }; + // Function to subscribe to a channel + const subscribeToChannel = async (channel: string, desiredChain: any) => { + if (!user) return { status: 'failure', message: 'User not initialised' }; + + if (chainId !== Number(desiredChain)) { + if (allowedNetworks[env].some((chain) => chain === Number(desiredChain))) + switchChain(Number(desiredChain)); + else { + frameRenderer.showMessageToast({ + toastTitle: 'Error', + toastMessage: 'Chain not supported', + toastType: 'ERROR', + getToastIcon: (size: any) => , + }); + return { status: 'failure', message: 'Chain not supported' }; + } + } + try { + const response = await user.notification.subscribe( + `eip155:${desiredChain}:${channel}` + ); + if (response.status === 204) + return { status: 'success', message: 'Subscribed' }; + } catch (error) { + frameRenderer.showMessageToast({ + toastTitle: 'Error', + toastMessage: 'Chain not supported', + toastType: 'ERROR', + getToastIcon: (size: any) => , + }); + return { status: 'failure', message: 'Something went wrong' }; + } + return { status: 'failure', message: 'Unexpected error' }; + }; + + // Function to trigger a transaction + const TriggerTx = async (data: any, provider: any) => { + console.log(allowedNetworks[env]); + console.log( + allowedNetworks[env].some( + (chain) => chain === Number(data.chainId.slice(7)) + ) + ); + if (chainId !== Number(data.chainId.slice(7))) { + if ( + allowedNetworks[env].some( + (chain) => chain === Number(data.chainId.slice(7)) + ) + ) + switchChain(Number(data.chainId.slice(7))); + else { + frameRenderer.showMessageToast({ + toastTitle: 'Error', + toastMessage: 'Chain not supported', + toastType: 'ERROR', + getToastIcon: (size: any) => , + }); + return { status: 'failure', message: 'Chain not supported' }; + } + } + let hash = undefined; + try { + const signer = provider.getUncheckedSigner(); + const tx = await signer.sendTransaction({ + from: account, + to: data.params.to, + value: data.params.value, + data: data.params.data, + chainId: Number(data.chainId.slice(7)), + }); + hash = tx.hash; + return { hash, status: 'success', message: 'Transaction sent' }; + } catch (error) { + frameRenderer.showMessageToast({ + toastTitle: 'Error', + toastMessage: 'Something went wrong', + toastType: 'ERROR', + getToastIcon: (size: any) => , + }); + return { + hash: 'Failed', + status: 'failure', + message: 'Something went wrong', + }; + } + }; + + // Function to handle button click on a frame button + const onButtonClick = async (button: { + index: string; + action?: string; + target?: string; + }) => { + if (button.action === 'mint') return; + let hash; + + // If the button action is post_redirect or link, opens the link in a new tab + if (button.action === 'post_redirect' || button.action === 'link') { + window.open(button.target!, '_blank'); + return; + } + + // If the button action is subscribe, subscribes to the channel and then makes a POST call to the Frame server + if (button.action?.includes('subscribe')) { + const desiredChainId = button.action?.split(':')[1]; + + const response = await subscribeToChannel(button.target!, desiredChainId); + if (response.status === 'failure') { + return; + } + } + + // If the button action is tx, triggers a transaction and then makes a POST call to the Frame server + if (button.action === 'tx' && button.target) { + const response = await fetch(`http://localhost:5004/${button.target}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + clientProtocol: 'push', + untrustedData: { + url: window.location.hostname, + unixTimestamp: Date.now(), + buttonIndex: Number(button.index), + inputText: inputText, + state: metaTags.state, + address: account, + }, + }), + }); + + const data = await response.json(); + + const { hash: txid, status } = await TriggerTx(data, provider); + hash = txid; + if (!txid || status === 'failure') return; + } + + // Makes a POST call to the Frame server after the action has been performed + const post_url = + button.action === 'tx' + ? metaTags.postURL + : button?.target!.startsWith('http') + ? button.target + : metaTags.postURL; + if (!post_url) return; + const response = await fetch(`http://localhost:5004/${post_url}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + origin: 'http://localhost:4200', + }, + body: JSON.stringify({ + clientProtocol: 'push', + untrustedData: { + url: window.location.hostname, + unixTimestamp: Date.now(), + buttonIndex: button.index, + inputText: inputText, + state: metaTags.state, + transactionId: hash, + }, + }), + }); + const data = await response.text(); + + const frameDetails: FrameDetails = getFormattedMetadata(url, data); + setInputText(''); + + setMetaTags(frameDetails); + }; + return ( +
+ {metaTags.image && ( +
+ + frame + + {metaTags?.input?.name && metaTags?.input?.content.length > 0 && ( +
+ { + setInputText(e.target.value); + }} + inputValue={inputText as string} + placeholder={metaTags.input.content} + /> +
+ )} +
+ {metaTags.buttons.map((button) => ( +
+ +
+ ))} +
+ +
+ )} +
+ ); +}; const MessageCard = ({ chat, position, isGroup, + account, }: { chat: IMessagePayload; position: number; isGroup: boolean; + account: string; }) => { const theme = useContext(ThemeContext); const time = moment(chat.timestamp).format('hh:mm a'); return ( +
+ {hasWebLink(chat.messageContent) && ( + + )} +
- {' '}
{chat.messageContent.split('\n').map((str) => ( { +export const ChatViewBubble = ({ + decryptedMessagePayload, +}: { + decryptedMessagePayload: IMessagePayload; +}) => { const { account } = useChatData(); const position = - pCAIP10ToWallet(decryptedMessagePayload.fromDID).toLowerCase() !== account?.toLowerCase() + pCAIP10ToWallet(decryptedMessagePayload.fromDID).toLowerCase() !== + account?.toLowerCase() ? 0 : 1; const { tweetId, messageType }: TwitterFeedReturnType = checkTwitterUrl({ message: decryptedMessagePayload?.messageContent, }); const [isGroup, setIsGroup] = useState(false); + useEffect(() => { if (decryptedMessagePayload.toDID.split(':')[0] === 'eip155') { if (isGroup) { @@ -363,13 +769,31 @@ export const ChatViewBubble = ({ decryptedMessagePayload }: { decryptedMessagePa } if (decryptedMessagePayload.messageType === 'GIF') { - return ; + return ( + + ); } if (decryptedMessagePayload.messageType === 'Image') { - return ; + return ( + + ); } if (decryptedMessagePayload.messageType === 'File') { - return ; + return ( + + ); } if (decryptedMessagePayload.messageType === 'TwitterFeedLink') { return ( @@ -381,7 +805,14 @@ export const ChatViewBubble = ({ decryptedMessagePayload }: { decryptedMessagePa /> ); } - return ; + return ( + + ); }; const FileDownloadIcon = styled.i` diff --git a/packages/uiweb/src/lib/components/chat/reusables/Button.tsx b/packages/uiweb/src/lib/components/chat/reusables/Button.tsx index 6bcd34ef9..cc290fe24 100644 --- a/packages/uiweb/src/lib/components/chat/reusables/Button.tsx +++ b/packages/uiweb/src/lib/components/chat/reusables/Button.tsx @@ -16,7 +16,6 @@ import styled, { ThemeProvider } from 'styled-components'; import { IChatTheme } from '../theme'; import { ThemeContext } from '../theme/ThemeProvider'; - export interface IButtonProps { width?: string; height?: string; @@ -31,8 +30,10 @@ type CustomStyleParamsType = { fontSize?: string; fontWeight?: string; padding?: string; - border?:string; - borderRadius?:string; + border?: string; + borderRadius?: string; + height?: string; + maxHeight?: string; }; /** @@ -48,15 +49,15 @@ export const Button: React.FC = (props) => { return ( - - {props.children} - + + {props.children} + ); }; @@ -66,7 +67,7 @@ const ChatButton = styled.button` flex-direction: column; justify-content: center; align-items: center; - gap:'2px' ; + gap: '2px'; padding: ${(props) => props.customStyle?.padding ? props.customStyle.padding : '16px'}; margin-top: 12px; @@ -83,7 +84,9 @@ const ChatButton = styled.button` ? props.customStyle.borderRadius : props.theme.borderRadius.modalInnerComponents}; border: ${(props) => - props.customStyle?.border ? props.customStyle.border : props.theme.border.modal}; + props.customStyle?.border + ? props.customStyle.border + : props.theme.border.modal}; font-size: 16px; font-weight: ${(props) => props.customStyle?.fontWeight ? props.customStyle.fontWeight : '500'}; diff --git a/packages/uiweb/src/lib/types/index.ts b/packages/uiweb/src/lib/types/index.ts index e7863509e..ac707e115 100644 --- a/packages/uiweb/src/lib/types/index.ts +++ b/packages/uiweb/src/lib/types/index.ts @@ -1,17 +1,22 @@ import type { ReactElement } from 'react'; import type { ENV } from '../config'; -import type { ParsedResponseType, IFeeds, Rules, PushAPI, } from '@pushprotocol/restapi'; +import type { + ParsedResponseType, + IFeeds, + Rules, + PushAPI, +} from '@pushprotocol/restapi'; import { Bytes, TypedDataDomain, TypedDataField, providers } from 'ethers'; export interface IMessageIPFS { - cid? : string; - chatId? :string; - event? :string; - from?:string; + cid?: string; + chatId?: string; + event?: string; + from?: string; message?: IMessage; meta?: any; - origin?:string; - reference? :string; + origin?: string; + reference?: string; to?: string[]; fromCAIP10: string; toCAIP10: string; @@ -94,30 +99,33 @@ export type viemSignerType = { provider?: any; }; -export type SignerType = ethersV5SignerType| ethersV6SignerType| viemSignerType;; +export type SignerType = + | ethersV5SignerType + | ethersV6SignerType + | viemSignerType; export type ParsedNotificationType = ParsedResponseType & { - channel:string; + channel: string; }; -export type NotificationFeedsType = { [key: string]:ParsedNotificationType}; -export type ChatFeedsType = { [key: string]:IFeeds}; +export type NotificationFeedsType = { [key: string]: ParsedNotificationType }; +export type ChatFeedsType = { [key: string]: IFeeds }; export interface Web3NameListType { [key: string]: string; } export const PUSH_TABS = { CHATS: 'CHATS', - APP_NOTIFICATIONS: 'APP_NOTIFICATIONS' + APP_NOTIFICATIONS: 'APP_NOTIFICATIONS', } as const; export const SOCKET_TYPE = { CHAT: 'chat', - NOTIFICATION: 'notification' + NOTIFICATION: 'notification', } as const; export const PUSH_SUB_TABS = { REQUESTS: 'REQUESTS', - SPAM:'SPAM' + SPAM: 'SPAM', } as const; export const LOCAL_STORAGE_KEYS = { @@ -128,55 +136,82 @@ export const SIDEBAR_PLACEHOLDER_KEYS = { CHAT: 'CHAT', SEARCH: 'SEARCH', NOTIFICATION: 'NOTIFICATION', - NEW_CHAT: 'NEW_CHAT' + NEW_CHAT: 'NEW_CHAT', } as const; -export type SidebarPlaceholderKeys = (typeof SIDEBAR_PLACEHOLDER_KEYS)[keyof typeof SIDEBAR_PLACEHOLDER_KEYS]; +export type SidebarPlaceholderKeys = + typeof SIDEBAR_PLACEHOLDER_KEYS[keyof typeof SIDEBAR_PLACEHOLDER_KEYS]; -export type LocalStorageKeys = (typeof LOCAL_STORAGE_KEYS)[keyof typeof LOCAL_STORAGE_KEYS]; -export type PushTabs = (typeof PUSH_TABS)[keyof typeof PUSH_TABS]; -export type PushSubTabs = (typeof PUSH_SUB_TABS)[keyof typeof PUSH_SUB_TABS]; -export type SocketType = (typeof SOCKET_TYPE)[keyof typeof SOCKET_TYPE]; +export type LocalStorageKeys = + typeof LOCAL_STORAGE_KEYS[keyof typeof LOCAL_STORAGE_KEYS]; +export type PushTabs = typeof PUSH_TABS[keyof typeof PUSH_TABS]; +export type PushSubTabs = typeof PUSH_SUB_TABS[keyof typeof PUSH_SUB_TABS]; +export type SocketType = typeof SOCKET_TYPE[keyof typeof SOCKET_TYPE]; export interface FileMessageContent { - content: string - name: string - type: string - size: number + content: string; + name: string; + type: string; + size: number; } -export type Messagetype = { messages: IMessageIPFS[]; lastThreadHash: string | null }; +export type Messagetype = { + messages: IMessageIPFS[]; + lastThreadHash: string | null; +}; export interface IGroup { - members: { wallet: string, publicKey: string, isAdmin: boolean, image: string }[], - pendingMembers: { wallet: string, publicKey: string, isAdmin: boolean, image: string }[], - contractAddressERC20: string | null, - numberOfERC20: number, - contractAddressNFT: string | null, - numberOfNFTTokens: number, - verificationProof: string, - groupImage: string | null, - groupName: string, - isPublic: boolean, - groupDescription: string | null, - groupCreator: string, - chatId: string, - groupType?:string | undefined, - rules?: Rules | null, + members: { + wallet: string; + publicKey: string; + isAdmin: boolean; + image: string; + }[]; + pendingMembers: { + wallet: string; + publicKey: string; + isAdmin: boolean; + image: string; + }[]; + contractAddressERC20: string | null; + numberOfERC20: number; + contractAddressNFT: string | null; + numberOfNFTTokens: number; + verificationProof: string; + groupImage: string | null; + groupName: string; + isPublic: boolean; + groupDescription: string | null; + groupCreator: string; + chatId: string; + groupType?: string | undefined; + rules?: Rules | null; } - export const MODAL_BACKGROUND_TYPE = { - OVERLAY:'OVERLAY', + OVERLAY: 'OVERLAY', BLUR: 'BLUR', TRANSPARENT: 'TRANSPARENT', +} as const; - } as const; - - export type ModalBackgroundType = keyof typeof MODAL_BACKGROUND_TYPE; +export type ModalBackgroundType = keyof typeof MODAL_BACKGROUND_TYPE; - export const MODAL_POSITION_TYPE = { - RELATIVE:'RELATIVE', - GLOBAL: 'GLOBAL', - } as const; - - export type ModalPositionType = keyof typeof MODAL_POSITION_TYPE; \ No newline at end of file +export const MODAL_POSITION_TYPE = { + RELATIVE: 'RELATIVE', + GLOBAL: 'GLOBAL', +} as const; + +export type ModalPositionType = keyof typeof MODAL_POSITION_TYPE; + +export interface FrameDetails { + image: string; + siteURL: string; + postURL: string; + buttons: { + index: string; + content: string; + action?: string; + target?: string; + }[]; + input?: { name: string; content: string }; + state?: string; +} diff --git a/packages/uiweb/src/lib/utilities/extractWebLink.ts b/packages/uiweb/src/lib/utilities/extractWebLink.ts new file mode 100644 index 000000000..547404981 --- /dev/null +++ b/packages/uiweb/src/lib/utilities/extractWebLink.ts @@ -0,0 +1,5 @@ +export function extractWebLink(message: string): string | null { + const webLinkRegex = /(https?:\/\/[^\s]+)/; + const match = message.match(webLinkRegex); + return match ? match[0] : null; +} diff --git a/packages/uiweb/src/lib/utilities/getFormattedMetadata.ts b/packages/uiweb/src/lib/utilities/getFormattedMetadata.ts new file mode 100644 index 000000000..3503e6cbc --- /dev/null +++ b/packages/uiweb/src/lib/utilities/getFormattedMetadata.ts @@ -0,0 +1,106 @@ +import { FrameDetails } from '../types'; + +export function getFormattedMetadata(URL: string, data: any) { + const frameDetails: FrameDetails = { + image: '', + siteURL: URL, + postURL: '', + buttons: [], + input: { name: '', content: '' }, + state: '', + }; + + const parser = new DOMParser(); + const doc = parser.parseFromString(data, 'text/html'); + + const metaElements: NodeListOf = + doc.head.querySelectorAll('meta'); + + metaElements.forEach((element, index) => { + const name = + element.getAttribute('name') || element.getAttribute('property'); + const content = element.getAttribute('content'); + + if (name && content) { + if (name === 'fc:frame:image' || name === 'of:image') { + frameDetails.image = content; + } else if (name === 'fc:frame:post_url' || name === 'of:post_url') { + frameDetails.postURL = content; + } else if (name === 'fc:frame:state' || name === 'of:state') { + frameDetails.state = content; + } else if ( + (name.includes('fc:frame:button') || name.includes('of:button')) && + !name.includes('action') && + !name.includes('target') + ) { + const index = name.split(':')[3]; + const indexZeroExists = frameDetails.buttons.some( + (button: any) => button.index === index + ); + if (!indexZeroExists) { + frameDetails.buttons.push({ + index: index, + content: content, + action: '', + target: '', + }); + } else { + const indexToUpdate = frameDetails.buttons.findIndex( + (button: any) => button.index === String(index) + ); + frameDetails.buttons[indexToUpdate].content = content; + frameDetails.buttons[indexToUpdate].index = index; + } + } else if (name === 'fc:frame:input:text' || name === 'of:input:text') { + frameDetails.input = { name, content }; + } else if ( + (name.includes('fc:frame:button') || name.includes('of:button')) && + name.includes(':action') + ) { + const number = name.split(':')[3]; + const indexZeroExists = frameDetails.buttons.some( + (button: any) => button.index === number + ); + if (!indexZeroExists) { + frameDetails.buttons.push({ + index: number, + content: '', + action: content, + target: '', + }); + } else { + const indexToUpdate = frameDetails.buttons.findIndex( + (button: any) => button.index === number + ); + frameDetails.buttons[indexToUpdate].action = content; + } + } else if ( + (name.includes('fc:frame:button') || name.includes('of:button')) && + name.includes(':target') + ) { + const number = name.split(':')[3]; + + const indexZeroExists = frameDetails.buttons.some( + (button: any) => button.index === number + ); + + if (!indexZeroExists) { + frameDetails.buttons.push({ + index: number, + content: '', + action: '', + target: content, + }); + } else { + const indexToUpdate = frameDetails.buttons.findIndex( + (button: any) => button.index === number + ); + + frameDetails.buttons[indexToUpdate].target = content; + } + } + } + }); + + return frameDetails; +} diff --git a/packages/uiweb/src/lib/utilities/hasWebLink.ts b/packages/uiweb/src/lib/utilities/hasWebLink.ts new file mode 100644 index 000000000..08dbba389 --- /dev/null +++ b/packages/uiweb/src/lib/utilities/hasWebLink.ts @@ -0,0 +1,5 @@ +export function hasWebLink(message: string): boolean { + const webLinkRegex = /(https?:\/\/[^\s]+)/; + const match = message.match(webLinkRegex); + return !!match; +} diff --git a/packages/uiweb/src/lib/utilities/index.ts b/packages/uiweb/src/lib/utilities/index.ts index d1269e57b..9740dad14 100644 --- a/packages/uiweb/src/lib/utilities/index.ts +++ b/packages/uiweb/src/lib/utilities/index.ts @@ -3,4 +3,7 @@ export * from './media'; export * from './formatbody'; export * from './time'; export * from './ipfs'; -export * from './formatType'; \ No newline at end of file +export * from './formatType'; +export * from './hasWebLink'; +export * from './extractWebLink'; +export * from './getFormattedMetadata'; From 70773ff1f1b80771ca16a1d588af12aca4a247dc Mon Sep 17 00:00:00 2001 From: Siddesh Date: Wed, 27 Mar 2024 12:51:54 +0530 Subject: [PATCH 02/16] fix: fix body req --- .../chat/ChatViewBubble/ChatViewBubble.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index f55e1b4de..1ff6a99c8 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -33,7 +33,7 @@ import { } from '../../../utilities'; import { Button, TextInput } from '../reusables'; import { MdError, MdOpenInNew } from 'react-icons/md'; -import { FaBell, FaLink } from 'react-icons/fa'; +import { FaBell, FaLink, FaRegThumbsUp } from 'react-icons/fa'; import { BsLightning } from 'react-icons/bs'; const SenderMessageAddress = ({ chat }: { chat: IMessagePayload }) => { @@ -257,7 +257,15 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { `eip155:${desiredChain}:${channel}` ); if (response.status === 204) - return { status: 'success', message: 'Subscribed' }; + frameRenderer.showMessageToast({ + toastTitle: 'Success', + toastMessage: 'Subscribed Successfully', + toastType: 'SUCCESS', + getToastIcon: (size: any) => ( + + ), + }); + return { status: 'success', message: 'Subscribed' }; } catch (error) { frameRenderer.showMessageToast({ toastTitle: 'Error', @@ -393,7 +401,7 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { untrustedData: { url: window.location.hostname, unixTimestamp: Date.now(), - buttonIndex: button.index, + buttonIndex: Number(button.index), inputText: inputText, state: metaTags.state, transactionId: hash, From 4efd775d3754cc66663099108e9ff3e2883d0d8b Mon Sep 17 00:00:00 2001 From: Siddesh Date: Wed, 27 Mar 2024 13:59:18 +0530 Subject: [PATCH 03/16] fix(\): use proxy server --- .../chat/ChatViewBubble/ChatViewBubble.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index 1ff6a99c8..300e32f52 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -125,6 +125,7 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { const { env, user } = useChatData(); const { chainId, switchChain, provider } = useAccount({ env }); const frameRenderer = useToast(); + const proxyServer = 'https://cors-m0l8.onrender.com'; const [metaTags, setMetaTags] = useState({ image: '', siteURL: '', @@ -138,8 +139,12 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { useEffect(() => { const fetchMetaTags = async (url: string) => { try { - const response = await fetch(url, { + const response = await fetch(`${proxyServer}/${url}`, { method: 'GET', + headers: { + 'Content-Type': 'application/json', + Origin: window.location.origin, + }, }); const htmlText = await response.text(); @@ -280,12 +285,6 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { // Function to trigger a transaction const TriggerTx = async (data: any, provider: any) => { - console.log(allowedNetworks[env]); - console.log( - allowedNetworks[env].some( - (chain) => chain === Number(data.chainId.slice(7)) - ) - ); if (chainId !== Number(data.chainId.slice(7))) { if ( allowedNetworks[env].some( @@ -357,10 +356,11 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { // If the button action is tx, triggers a transaction and then makes a POST call to the Frame server if (button.action === 'tx' && button.target) { - const response = await fetch(`http://localhost:5004/${button.target}`, { + const response = await fetch(`${proxyServer}/${button.target}`, { method: 'POST', headers: { 'Content-Type': 'application/json', + Origin: window.location.origin, }, body: JSON.stringify({ clientProtocol: 'push', @@ -390,16 +390,16 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { ? button.target : metaTags.postURL; if (!post_url) return; - const response = await fetch(`http://localhost:5004/${post_url}`, { + const response = await fetch(`${proxyServer}/${post_url}`, { method: 'POST', headers: { 'Content-Type': 'application/json', - origin: 'http://localhost:4200', + Origin: window.location.origin, }, body: JSON.stringify({ clientProtocol: 'push', untrustedData: { - url: window.location.hostname, + url: window.location.href, unixTimestamp: Date.now(), buttonIndex: Number(button.index), inputText: inputText, From e46ee4f73b4ff129311e54df4b8965e7a515863a Mon Sep 17 00:00:00 2001 From: Siddesh Date: Thu, 4 Apr 2024 20:33:40 +0530 Subject: [PATCH 04/16] feat(uiweb): add trustedData to outgoing frame request --- packages/uiweb/package.json | 4 +- .../chat/ChatViewBubble/ChatViewBubble.tsx | 68 ++++++++++++++++--- packages/uiweb/src/lib/helpers/chat/index.ts | 3 +- packages/uiweb/src/lib/helpers/chat/pgp.ts | 56 +++++++++++++++ 4 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 packages/uiweb/src/lib/helpers/chat/pgp.ts diff --git a/packages/uiweb/package.json b/packages/uiweb/package.json index d191a2d3b..294898ea8 100644 --- a/packages/uiweb/package.json +++ b/packages/uiweb/package.json @@ -23,8 +23,10 @@ "html-react-parser": "^1.4.13", "livekit-client": "^1.13.3", "moment": "^2.29.4", - "react-icons": "^4.10.1", + "openpgp": "^5.11.1", + "protobufjs": "^7.2.6", "react-easy-crop": "^4.1.4", + "react-icons": "^4.10.1", "react-image-file-resizer": "^0.4.7", "react-toastify": "^9.1.3", "react-twitter-embed": "^4.0.4", diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index 300e32f52..7f25a877b 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -25,6 +25,8 @@ import { getPfp, pCAIP10ToWallet, shortenText, + sign, + toSerialisedHexString, } from '../../../helpers'; import { extractWebLink, @@ -35,6 +37,10 @@ import { Button, TextInput } from '../reusables'; import { MdError, MdOpenInNew } from 'react-icons/md'; import { FaBell, FaLink, FaRegThumbsUp } from 'react-icons/fa'; import { BsLightning } from 'react-icons/bs'; +import { + ChatMainStateContext, + ChatMainStateContextType, +} from '../../../context/chatAndNotification/chat/chatMainStateContext'; const SenderMessageAddress = ({ chat }: { chat: IMessagePayload }) => { const { account } = useContext(ChatDataContext); @@ -121,9 +127,19 @@ const MessageWrapper = ({
); }; -const FrameRenderer = ({ url, account }: { url: string; account: string }) => { - const { env, user } = useChatData(); +const FrameRenderer = ({ + url, + account, + messageId, +}: { + url: string; + account: string; + messageId: string; +}) => { + const { env, user, pgpPrivateKey } = useChatData(); const { chainId, switchChain, provider } = useAccount({ env }); + const { selectedChatId } = + useContext(ChatMainStateContext); const frameRenderer = useToast(); const proxyServer = 'https://cors-m0l8.onrender.com'; const [metaTags, setMetaTags] = useState({ @@ -338,6 +354,25 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { if (button.action === 'mint') return; let hash; + const serializedProtoMessage = await toSerialisedHexString({ + url: url, + unixTimestamp: Date.now(), + buttonIndex: Number(button.index), + inputText: inputText ?? 'undefined', + state: metaTags.state ?? 'undefined', + transactionId: hash ?? 'undefined', + address: account, + messageId: messageId, + chatId: window.location.href.split('/').pop() ?? 'null', + clientProtocol: 'push', + }); + const signedMessage = await sign({ + message: serializedProtoMessage, + signingKey: pgpPrivateKey!, + }); + + console.log(signedMessage); + // If the button action is post_redirect or link, opens the link in a new tab if (button.action === 'post_redirect' || button.action === 'link') { window.open(button.target!, '_blank'); @@ -365,12 +400,19 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { body: JSON.stringify({ clientProtocol: 'push', untrustedData: { - url: window.location.hostname, + url: url, unixTimestamp: Date.now(), buttonIndex: Number(button.index), - inputText: inputText, - state: metaTags.state, + inputText: inputText ?? 'undefined', + state: metaTags.state ?? 'undefined', + transactionId: hash ?? 'undefined', address: account, + messageId: messageId, + chatId: selectedChatId ?? 'null', + }, + trustedData: { + messageBytes: serializedProtoMessage, + pgpSignature: signedMessage, }, }), }); @@ -399,12 +441,19 @@ const FrameRenderer = ({ url, account }: { url: string; account: string }) => { body: JSON.stringify({ clientProtocol: 'push', untrustedData: { - url: window.location.href, + url: url, unixTimestamp: Date.now(), buttonIndex: Number(button.index), - inputText: inputText, - state: metaTags.state, - transactionId: hash, + inputText: inputText ?? 'undefined', + state: metaTags.state ?? 'undefined', + transactionId: hash ?? 'undefined', + address: account, + messageId: messageId, + chatId: selectedChatId ?? 'null', + }, + trustedData: { + messageBytes: serializedProtoMessage, + pgpSignature: signedMessage, }, }), }); @@ -532,6 +581,7 @@ const MessageCard = ({ )}
diff --git a/packages/uiweb/src/lib/helpers/chat/index.ts b/packages/uiweb/src/lib/helpers/chat/index.ts index 48d7007d6..b5615d704 100644 --- a/packages/uiweb/src/lib/helpers/chat/index.ts +++ b/packages/uiweb/src/lib/helpers/chat/index.ts @@ -3,4 +3,5 @@ export * from './search'; export * from './time'; export * from './user'; export * from './utility'; -export * from './localStorage'; \ No newline at end of file +export * from './localStorage'; +export * from './pgp'; diff --git a/packages/uiweb/src/lib/helpers/chat/pgp.ts b/packages/uiweb/src/lib/helpers/chat/pgp.ts new file mode 100644 index 000000000..288a890f2 --- /dev/null +++ b/packages/uiweb/src/lib/helpers/chat/pgp.ts @@ -0,0 +1,56 @@ +import * as openpgp from 'openpgp'; +import * as protobuf from 'protobufjs'; +export const toSerialisedHexString = async (data: { + url: string; + unixTimestamp: number; + buttonIndex: number; + inputText: string; + state: string; + transactionId: string; + address: string; + messageId: string; + chatId: string; + clientProtocol: string; +}) => { + const protoDefinition = ` + syntax = "proto3"; + + message ChatMessage { + string url = 1; + int64 unixTimestamp = 2; + int32 buttonIndex = 3; + string inputText = 4; + string state = 5; + string transactionId = 6; + string address = 7; + string messageId = 8; + string chatId = 9; + string clientProtocol = 10; + } + `; + + // Load the message + const root = protobuf.parse(protoDefinition); + const ChatMessage = root.root.lookupType('ChatMessage'); + const chatMessage = ChatMessage.create(data); + const binaryData = ChatMessage.encode(chatMessage).finish(); + const hexString = Buffer.from(binaryData).toString('hex'); + + return hexString; +}; +export const sign = async ({ + message, + signingKey, +}: { + message: string; + signingKey: string; +}): Promise => { + const messageObject = await openpgp.createMessage({ text: message }); + const privateKey = await openpgp.readPrivateKey({ armoredKey: signingKey }); + const signature = await openpgp.sign({ + message: messageObject, + signingKeys: privateKey, + detached: true, + }); + return signature; +}; From 70c3adbd09fc0abd2cf72093165805f4309cebb1 Mon Sep 17 00:00:00 2001 From: Siddesh Date: Fri, 5 Apr 2024 17:11:13 +0530 Subject: [PATCH 05/16] feat(uiweb): handle different frame spec --- .../chat/ChatViewBubble/ChatViewBubble.tsx | 185 ++++++++----- .../lib/components/chat/reusables/Button.tsx | 3 + packages/uiweb/src/lib/helpers/chat/pgp.ts | 6 +- packages/uiweb/src/lib/types/index.ts | 27 +- .../src/lib/utilities/getFormattedMetadata.ts | 242 ++++++++++++------ 5 files changed, 307 insertions(+), 156 deletions(-) diff --git a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx index 7f25a877b..6b2fa5d0c 100644 --- a/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx +++ b/packages/uiweb/src/lib/components/chat/ChatViewBubble/ChatViewBubble.tsx @@ -17,7 +17,12 @@ import { ChatDataContext } from '../../../context'; import { useAccount, useChatData } from '../../../hooks'; import { ThemeContext } from '../theme/ThemeProvider'; -import { FileMessageContent, FrameDetails } from '../../../types'; +import { + FileMessageContent, + FrameButton, + FrameDetails, + IFrame, +} from '../../../types'; import { IMessagePayload, TwitterFeedReturnType } from '../exportedTypes'; import { allowedNetworks, ENV, FILE_ICON } from '../../../config'; import { @@ -142,14 +147,9 @@ const FrameRenderer = ({ useContext(ChatMainStateContext); const frameRenderer = useToast(); const proxyServer = 'https://cors-m0l8.onrender.com'; - const [metaTags, setMetaTags] = useState({ - image: '', - siteURL: '', - postURL: '', - buttons: [], - input: { name: '', content: '' }, - }); + const [FrameData, setFrameData] = useState