Skip to content

Commit

Permalink
better error messages for frames (#1080)
Browse files Browse the repository at this point in the history
  • Loading branch information
JFrankfurt authored Oct 17, 2024
1 parent fc99fae commit 12b40b5
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand All @@ -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<typeof useFrame>[0],
'homeframeUrl' | 'signerState' | 'frameContext'
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -296,8 +282,6 @@ export function FramesProvider({ children }: FramesProviderProps) {
onConnectWallet: openConnectModal,
frameContext: farcasterFrameContext,
},
frameInteractionError,
setFrameInteractionError,
showFarcasterQRModal,
setShowFarcasterQRModal,
pendingFrameChange,
Expand All @@ -317,7 +301,6 @@ export function FramesProvider({ children }: FramesProviderProps) {
onSignature,
openConnectModal,
farcasterFrameContext,
frameInteractionError,
showFarcasterQRModal,
pendingFrameChange,
addFrame,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>('');
const clearError = useCallback(() => setError(''), []);
const [isDismissing, setIsDismissing] = useState<boolean>(false);
const handleDismissError = () => {
setIsDismissing(true);
setTimeout(() => {
clearError();
setIsDismissing(false);
}, 200); // Match the fade-out duration
};

const fetchFn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const queryKey = ['frame-data', input];
Expand All @@ -28,6 +59,51 @@ export default function Frame({ url, className }: FrameProps) {
});
};

const useSharedCallback = <T extends SignatureData | TransactionData>(
callback: ((a: T) => Promise<null | `0x${string}`>) | 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,
Expand All @@ -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,
Expand Down Expand Up @@ -79,5 +156,23 @@ export default function Frame({ url, className }: FrameProps) {
[className],
);

return <FrameUI frameState={frameState} theme={aggregatedTheme} components={components} />;
return (
<div className="relative">
<Transition
show={!!error && !isDismissing}
enter="transition-opacity ease-out duration-200"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
onClick={handleDismissError}
className="absolute left-1/2 top-1/2 z-50 flex -translate-x-1/2 -translate-y-1/2 transform cursor-pointer items-center justify-center gap-2 rounded-xl border border-red-30 bg-red-0 px-2 py-3 text-xs font-medium text-palette-negative shadow-lg"
afterLeave={clearError}
>
<InformationCircleIcon width={16} height={16} /> {error}
</Transition>
<FrameUI frameState={frameState} theme={aggregatedTheme} components={components} />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,18 @@ 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';
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);
Expand Down Expand Up @@ -80,15 +70,6 @@ function SectionContent() {
</Link>
)}
</div>
{frameInteractionError && (
<Button
size={ButtonSizes.Small}
onClick={handleErrorClick}
className="text-sm text-state-n-hovered"
>
{frameInteractionError}
</Button>
)}
<div className="columns-1 p-4 xl:columns-2">
{frameUrls.map((url) => (
<FrameListItem url={url} key={url} />
Expand Down

0 comments on commit 12b40b5

Please sign in to comment.