From c8da54947a3ea9857cfdf4de20fc77f406377bbf Mon Sep 17 00:00:00 2001 From: Jordan Frankfurt Date: Tue, 15 Oct 2024 10:41:48 -0500 Subject: [PATCH] better error messages for frames --- .../UsernameProfileSectionFrames/Context.tsx | 33 ++---- .../UsernameProfileSectionFrames/Frame.tsx | 103 +++++++++++++++++- .../UsernameProfileSectionFrames/index.tsx | 23 +--- 3 files changed, 109 insertions(+), 50 deletions(-) diff --git a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context.tsx b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context.tsx index f34fbf6dfb..6230c25669 100644 --- a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context.tsx +++ b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context.tsx @@ -28,14 +28,14 @@ import { namehash } from 'viem'; import { useAccount, useChainId, useConfig, useWriteContract } from 'wagmi'; import { sendTransaction, signTypedData, switchChain } from 'wagmi/actions'; -class InvalidChainIdError extends Error {} -class CouldNotChangeChainError extends Error {} +export class InvalidChainIdError extends Error {} +export class CouldNotChangeChainError extends Error {} function isValidChainId(id: string): boolean { return id.startsWith('eip155:'); } -function parseChainId(id: string): number { +export function parseChainId(id: string): number { if (!isValidChainId(id)) { throw new InvalidChainIdError(`Invalid chainId ${id}`); } @@ -52,8 +52,6 @@ function removeUrl(urls: string, urlSubstringToRemove: string): string { export type FrameContextValue = { currentWalletIsProfileOwner?: boolean; frameUrlRecord: string; - frameInteractionError: string; - setFrameInteractionError: (s: string) => void; frameConfig: Omit< Parameters[0], 'homeframeUrl' | 'signerState' | 'frameContext' @@ -117,7 +115,6 @@ export function FramesProvider({ children }: FramesProviderProps) { const currentChainId = useChainId(); const config = useConfig(); const { openConnectModal } = useConnectModal(); - const [frameInteractionError, setFrameInteractionError] = useState(''); const onTransaction: OnTransactionFunc = useCallback( async ({ transactionData, frame }) => { @@ -148,18 +145,15 @@ export function FramesProvider({ children }: FramesProviderProps) { return transactionId; } catch (error) { if (error instanceof InvalidChainIdError) { - setFrameInteractionError('Invalid chain id'); logEventWithContext('basename_profile_frame_invalid_chain_id', ActionType.error); } else if (error instanceof CouldNotChangeChainError) { logEventWithContext('basename_profile_frame_could_not_change_chain', ActionType.error); - setFrameInteractionError(`Must switch chain to ${requestedChainId}`); } else { - setFrameInteractionError('Error sending transaction'); + logError(error, `${frame.postUrl ?? frame.title} failed to complete a frame transaction`); } - logError(error, `${frame.postUrl ?? frame.title} failed to complete a frame transaction`); - - return null; + // intentional re-throw to be caught by individual frames + throw error; } }, [address, config, currentChainId, openConnectModal, logEventWithContext, logError], @@ -191,17 +185,9 @@ export function FramesProvider({ children }: FramesProviderProps) { return await signTypedData(config, params); } catch (error) { - if (error instanceof InvalidChainIdError) { - setFrameInteractionError('Invalid chain id'); - } else if (error instanceof CouldNotChangeChainError) { - setFrameInteractionError('Could not change chain'); - } else { - setFrameInteractionError('Error signing data'); - } - logError(error, `${frame.postUrl ?? frame.title} failed to sign frame data`); - - return null; + // intentional re-throw to be caught by individual frames + throw error; } }, [address, openConnectModal, currentChainId, config, logError], @@ -296,8 +282,6 @@ export function FramesProvider({ children }: FramesProviderProps) { onConnectWallet: openConnectModal, frameContext: farcasterFrameContext, }, - frameInteractionError, - setFrameInteractionError, showFarcasterQRModal, setShowFarcasterQRModal, pendingFrameChange, @@ -317,7 +301,6 @@ export function FramesProvider({ children }: FramesProviderProps) { onSignature, openConnectModal, farcasterFrameContext, - frameInteractionError, showFarcasterQRModal, pendingFrameChange, addFrame, diff --git a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Frame.tsx b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Frame.tsx index 0eb7453501..bac4f54cbd 100644 --- a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Frame.tsx +++ b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/Frame.tsx @@ -1,22 +1,53 @@ +import { OnSignatureFunc, OnTransactionFunc } from '@frames.js/render'; import { FrameUI } from '@frames.js/render/ui'; import { useFrame } from '@frames.js/render/use-frame'; +import { Transition } from '@headlessui/react'; +import { InformationCircleIcon } from '@heroicons/react/16/solid'; import { useQueryClient } from '@tanstack/react-query'; -import { useFrameContext } from 'apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context'; +import { + CouldNotChangeChainError, + InvalidChainIdError, + parseChainId, + useFrameContext, +} from 'apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context'; import { components, theme, } from 'apps/web/src/components/Basenames/UsernameProfileSectionFrames/FrameTheme'; import cn from 'classnames'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; type FrameProps = { url: string; className?: string; }; +// Define distinct types for signature and transaction data +type SignatureData = { + signatureData: { + chainId: string; + }; +}; + +type TransactionData = { + transactionData: { + chainId: string; + }; +}; + export default function Frame({ url, className }: FrameProps) { - const { frameConfig, farcasterSignerState, anonSignerState } = useFrameContext(); + const { frameConfig: sharedConfig, farcasterSignerState, anonSignerState } = useFrameContext(); const queryClient = useQueryClient(); + const [error, setError] = useState(''); + const clearError = useCallback(() => setError(''), []); + const [isDismissing, setIsDismissing] = useState(false); + const handleDismissError = () => { + setIsDismissing(true); + setTimeout(() => { + clearError(); + setIsDismissing(false); + }, 200); // Match the fade-out duration + }; const fetchFn = async (input: RequestInfo | URL, init?: RequestInit): Promise => { const queryKey = ['frame-data', input]; @@ -28,6 +59,51 @@ export default function Frame({ url, className }: FrameProps) { }); }; + const useSharedCallback = ( + callback: ((a: T) => Promise) | undefined, + ) => + useCallback( + async (a: T) => { + if (!callback) return null; + try { + return await callback(a); + } catch (err) { + const signatureData = 'signatureData' in a && a.signatureData; + const transactionData = 'transactionData' in a && a.transactionData; + if (err instanceof InvalidChainIdError) { + setError('Invalid chain id'); + } else if (err instanceof CouldNotChangeChainError) { + const chainId = + 'signatureData' in a ? a.signatureData.chainId : a.transactionData.chainId; + const requestedChainId = parseChainId(chainId); + setError(`Must switch chain to ${requestedChainId}`); + } else { + if (signatureData) { + setError('Error signing data'); + } else if (transactionData) { + setError('Error sending transaction'); + } else { + setError('Error processing data'); + } + } + return null; + } + }, + [callback], + ); + + const onSignature: OnSignatureFunc = useSharedCallback(sharedConfig.onSignature); + const onTransaction: OnTransactionFunc = useSharedCallback(sharedConfig.onTransaction); + + const frameConfig = useMemo( + () => ({ + ...sharedConfig, + onSignature, + onTransaction, + }), + [onSignature, onTransaction, sharedConfig], + ); + const farcasterFrameState = useFrame({ ...frameConfig, homeframeUrl: url, @@ -36,6 +112,7 @@ export default function Frame({ url, className }: FrameProps) { // @ts-expect-error frames.js uses node.js Response typing here instead of web Response fetchFn, }); + const openFrameState = useFrame({ ...frameConfig, homeframeUrl: url, @@ -79,5 +156,23 @@ export default function Frame({ url, className }: FrameProps) { [className], ); - return ; + return ( +
+ + {error} + + +
+ ); } diff --git a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/index.tsx b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/index.tsx index 4759423879..4a9e1ab394 100644 --- a/apps/web/src/components/Basenames/UsernameProfileSectionFrames/index.tsx +++ b/apps/web/src/components/Basenames/UsernameProfileSectionFrames/index.tsx @@ -8,7 +8,7 @@ import { } from 'apps/web/src/components/Basenames/UsernameProfileSectionFrames/Context'; import FarcasterAccountModal from 'apps/web/src/components/Basenames/UsernameProfileSectionFrames/FarcasterAccountModal'; import FrameListItem from 'apps/web/src/components/Basenames/UsernameProfileSectionFrames/FrameListItem'; -import { Button, ButtonSizes } from 'apps/web/src/components/Button/Button'; +import { Icon } from 'apps/web/src/components/Icon/Icon'; import ImageAdaptive from 'apps/web/src/components/ImageAdaptive'; import { ActionType } from 'libs/base-ui/utils/logEvent'; import { StaticImageData } from 'next/image'; @@ -16,20 +16,10 @@ import Link from 'next/link'; import { useCallback } from 'react'; import cornerGarnish from './corner-garnish.svg'; import frameIcon from './frame-icon.svg'; -import { Icon } from 'apps/web/src/components/Icon/Icon'; function SectionContent() { const { profileUsername, currentWalletIsProfileOwner } = useUsernameProfile(); - const { - frameInteractionError, - setFrameInteractionError, - frameUrls, - existingTextRecordsIsLoading, - } = useFrameContext(); - const handleErrorClick = useCallback( - () => setFrameInteractionError(''), - [setFrameInteractionError], - ); + const { frameUrls, existingTextRecordsIsLoading } = useFrameContext(); const { logEventWithContext } = useAnalytics(); const handleAddFrameLinkClick = useCallback(() => { logEventWithContext('basename_profile_frame_try_now_clicked', ActionType.click); @@ -80,15 +70,6 @@ function SectionContent() { )} - {frameInteractionError && ( - - )}
{frameUrls.map((url) => (