Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: better error messages for frames #1080

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is v hard to parse/read, maybe add comment or split?

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
Loading