diff --git a/docs/pages/reference/render/types.mdx b/docs/pages/reference/render/types.mdx index 85df5fe0..d0189930 100644 --- a/docs/pages/reference/render/types.mdx +++ b/docs/pages/reference/render/types.mdx @@ -12,20 +12,224 @@ import { SignerStateInstance } from "@frames.js/render"; import type { Frame, FrameButton, - ParsingReport, + FrameButtonLink, + FrameButtonPost, + FrameButtonTx, + SupportedParsingSpecification, TransactionTargetResponse, + TransactionTargetResponseSendTransaction, + TransactionTargetResponseSignTypedDataV4, getFrame, } from "frames.js"; -import type { FarcasterFrameContext } from "./farcaster/frames"; +import type { Dispatch } from "react"; +import type { ParseResult } from "frames.js/frame-parsers"; +import type { + CastActionResponse, + ComposerActionFormResponse, + ComposerActionState, +} from "frames.js/types"; +import type { FrameStackAPI } from "./use-frame-stack"; + +export type OnTransactionArgs = { + transactionData: TransactionTargetResponseSendTransaction; + /** If the transaction was triggered by a frame button, this will be the frame that it was from */ + frame?: Frame; + /** If the transaction was triggered by a frame button, this will be the frame button that triggered it */ + frameButton?: FrameButton; +}; export type OnTransactionFunc = ( - t: OnTransactionArgs + arg: OnTransactionArgs ) => Promise<`0x${string}` | null>; -export type UseFrameReturn< - SignerStorageType = object, - FrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - FrameContextType extends FrameContext = FarcasterFrameContext, +export type OnSignatureArgs = { + signatureData: TransactionTargetResponseSignTypedDataV4; + /** If the signature was triggered by a frame button, this will be the frame that it was from */ + frame?: Frame; + /** If the signature was triggered by a frame button, this will be the frame button that triggered it */ + frameButton?: FrameButton; +}; + +export type OnSignatureFunc = ( + args: OnSignatureArgs +) => Promise<`0x${string}` | null>; + +type OnComposerFormActionFuncArgs = { + form: ComposerActionFormResponse; + cast: ComposerActionState; +}; + +export type OnComposeFormActionFuncReturnType = + | { + /** + * Updated composer action state + */ + composerActionState: ComposerActionState; + } + | undefined; + +/** + * If the function resolves to undefined it means that the dialog was probably closed resulting in no operation at all. + */ +export type OnComposerFormActionFunc = ( + arg: OnComposerFormActionFuncArgs +) => Promise; + +/** + * Called when user presses transaction button but there is no wallet connected. + * + * After wallet is connect, "connectAddress" option on useFrame() should be set to the connected address. + */ +export type OnConnectWalletFunc = () => void; + +/** + * Used to sign frame action + */ +export type SignFrameActionFunc< + TSignerStorageType = Record, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameContextType extends FrameContext = FrameContext, +> = ( + actionContext: SignerStateActionContext +) => Promise>; + +export type UseFetchFrameSignFrameActionFunction< + TSignerStateActionContext extends SignerStateActionContext< + unknown, + Record + >, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, +> = (arg: { + actionContext: TSignerStateActionContext; + /** + * @defaultValue false + */ + forceRealSigner?: boolean; +}) => Promise>; + +export type UseFetchFrameOptions< + TSignerStorageType = Record, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameContextType extends FrameContext = FrameContext, +> = { + stackAPI: FrameStackAPI; + stackDispatch: React.Dispatch; + specification: SupportedParsingSpecification; + /** + * URL or path to the frame proxy handling GET requests. + */ + frameGetProxy: string; + /** + * URL or path to the frame proxy handling POST requests. + */ + frameActionProxy: string; + /** + * Extra payload to be sent with the POST request. + */ + extraButtonRequestPayload?: Record; + signFrameAction: UseFetchFrameSignFrameActionFunction< + SignerStateActionContext, + TFrameActionBodyType + >; + /** + * Called after transaction data has been returned from the server and user needs to approve the transaction. + */ + onTransaction: OnTransactionFunc; + /** Transaction data suffix */ + transactionDataSuffix?: `0x${string}`; + /** + * Called after transaction data has been returned from the server and user needs to sign the typed data. + */ + onSignature: OnSignatureFunc; + onComposerFormAction: OnComposerFormActionFunc; + /** + * This function can be used to customize how error is reported to the user. + * + * Should be memoized + */ + onError?: (error: Error) => void; + /** + * Custom fetch compatible function used to make requests. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API + */ + fetchFn: typeof fetch; + /** + * This function is called when the frame returns a redirect in response to post_redirect button click. + */ + onRedirect: (location: URL) => void; + /** + * Called when user presses the tx button just before the action is signed and sent to the server + * to obtain the transaction data. + */ + onTransactionDataStart?: (event: { button: FrameButtonTx }) => void; + /** + * Called when transaction data has been successfully returned from the server. + */ + onTransactionDataSuccess?: (event: { + button: FrameButtonTx; + data: TransactionTargetResponse; + }) => void; + /** + * Called when anything failed between onTransactionDataStart and obtaining the transaction data. + */ + onTransactionDataError?: (error: Error) => void; + /** + * Called before onTransaction() is called + * Called after onTransactionDataSuccess() is called + */ + onTransactionStart?: (event: { + button: FrameButtonTx; + data: TransactionTargetResponseSendTransaction; + }) => void; + /** + * Called when onTransaction() returns a transaction id + */ + onTransactionSuccess?: (event: { button: FrameButtonTx }) => void; + /** + * Called when onTransaction() fails to return a transaction id + */ + onTransactionError?: (error: Error) => void; + /** + * Called before onSignature() is called + * Called after onTransactionDataSuccess() is called + */ + onSignatureStart?: (event: { + button: FrameButtonTx; + data: TransactionTargetResponseSignTypedDataV4; + }) => void; + /** + * Called when onSignature() returns a transaction id + */ + onSignatureSuccess?: (event: { button: FrameButtonTx }) => void; + /** + * Called when onSignature() fails to return a transaction id + */ + onSignatureError?: (error: Error) => void; + /** + * Called after either onSignatureSuccess() or onTransactionSuccess() is called just before the transaction is sent to the server. + */ + onTransactionProcessingStart?: (event: { + button: FrameButtonTx; + transactionId: `0x${string}`; + }) => void; + /** + * Called after the transaction has been successfully sent to the server and returned a success response. + */ + onTransactionProcessingSuccess?: (event: { + button: FrameButtonTx; + transactionId: `0x${string}`; + }) => void; + /** + * Called when the transaction has been sent to the server but the server returned an error. + */ + onTransactionProcessingError?: (error: Error) => void; +}; + +export type UseFrameOptions< + TSignerStorageType = Record, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameContextType extends FrameContext = FrameContext, > = { /** skip frame signing, for frames that don't verify signatures */ dangerousSkipSigning?: boolean; @@ -35,22 +239,32 @@ export type UseFrameReturn< frameGetProxy: string; /** an signer state object used to determine what actions are possible */ signerState: SignerStateInstance< - SignerStorageType, - FrameActionBodyType, - FrameContextType + TSignerStorageType, + TFrameActionBodyType, + TFrameContextType >; /** the url of the homeframe, if null / undefined won't load a frame */ homeframeUrl: string | null | undefined; - /** the initial frame. if not specified will fetch it from the url prop */ - frame?: Frame; - /** connected wallet address of the user, send to the frame for transaction requests */ + /** the initial frame. if not specified will fetch it from the homeframeUrl prop */ + frame?: Frame | ParseResult; + /** + * connected wallet address of the user, send to the frame for transaction requests + */ connectedAddress: `0x${string}` | undefined; /** a function to handle mint buttons */ onMint?: (t: OnMintArgs) => void; - /** a function to handle transaction buttons, returns the transaction hash or null */ + /** a function to handle transaction buttons that returned transaction data from the target, returns the transaction hash or null */ onTransaction?: OnTransactionFunc; + /** Transaction data suffix */ + transactionDataSuffix?: `0x${string}`; + /** A function to handle transaction buttons that returned signature data from the target, returns signature hash or null */ + onSignature?: OnSignatureFunc; + /** + * Called when user presses transaction button but there is no wallet connected. + */ + onConnectWallet?: OnConnectWalletFunc; /** the context of this frame, used for generating Frame Action payloads */ - frameContext: FrameContextType; + frameContext: TFrameContextType; /** * Extra data appended to the frame action payload */ @@ -60,111 +274,424 @@ export type UseFrameReturn< * * @defaultValue 'farcaster' */ - specification?: SupportedParsingSpecification; + specification?: Exclude; + /** + * This function can be used to customize how error is reported to the user. + */ + onError?: (error: Error) => void; + /** + * This function can be used to customize how the link button click is handled. + */ + onLinkButtonClick?: (button: FrameButtonLink) => void; +} & Partial< + Pick< + UseFetchFrameOptions, + | "fetchFn" + | "onRedirect" + | "onComposerFormAction" + | "onTransactionDataError" + | "onTransactionDataStart" + | "onTransactionDataSuccess" + | "onTransactionError" + | "onTransactionStart" + | "onTransactionSuccess" + | "onSignatureError" + | "onSignatureStart" + | "onSignatureSuccess" + | "onTransactionProcessingError" + | "onTransactionProcessingStart" + | "onTransactionProcessingSuccess" + > +>; + +type SignerStateActionSharedContext< + TSignerStorageType = Record, + TFrameContextType extends FrameContext = FrameContext, +> = { + target?: string; + frameButton: FrameButton; + buttonIndex: number; + url: string; + inputText?: string; + signer: TSignerStorageType | null; + state?: string; + transactionId?: `0x${string}`; + address?: `0x${string}`; + /** Transacting address is not included in non-transaction frame actions */ + frameContext: TFrameContextType; +}; + +export type SignerStateDefaultActionContext< + TSignerStorageType = Record, + TFrameContextType extends FrameContext = FrameContext, +> = { + type?: "default"; +} & SignerStateActionSharedContext; + +export type SignerStateTransactionDataActionContext< + TSignerStorageType = Record, + TFrameContextType extends FrameContext = FrameContext, +> = { + type: "tx-data"; + /** Wallet address used to create the transaction, available only for "tx" button actions */ + address: `0x${string}`; +} & SignerStateActionSharedContext; + +export type SignerStateTransactionPostActionContext< + TSignerStorageType = Record, + TFrameContextType extends FrameContext = FrameContext, +> = { + type: "tx-post"; + /** Wallet address used to create the transaction, available only for "tx" button actions */ + address: `0x${string}`; + transactionId: `0x${string}`; +} & SignerStateActionSharedContext; + +export type SignerStateActionContext< + TSignerStorageType = Record, + TFrameContextType extends FrameContext = FrameContext, +> = + | SignerStateDefaultActionContext + | SignerStateTransactionDataActionContext< + TSignerStorageType, + TFrameContextType + > + | SignerStateTransactionPostActionContext< + TSignerStorageType, + TFrameContextType + >; + +export type SignedFrameAction< + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, +> = { + body: TFrameActionBodyType; + searchParams: URLSearchParams; }; +export type SignFrameActionFunction< + TSignerStateActionContext extends SignerStateActionContext< + unknown, + Record + > = SignerStateActionContext, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, +> = ( + actionContext: TSignerStateActionContext +) => Promise>; + export interface SignerStateInstance< - SignerStorageType = object, - FrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, - FrameContextType extends FrameContext = FarcasterFrameContext, + TSignerStorageType = Record, + TFrameActionBodyType extends FrameActionBodyPayload = FrameActionBodyPayload, + TFrameContextType extends FrameContext = FrameContext, > { - signer?: SignerStorageType | null; + /** + * For which specification is this signer required. + * + * If the value is an array it will take first valid specification if there is no valid specification + * it will return the first specification in array no matter the validity. + */ + readonly specification: + | SupportedParsingSpecification + | SupportedParsingSpecification[]; + signer: TSignerStorageType | null; + /** + * True only if signer is approved or impersonating + */ hasSigner: boolean; - signFrameAction: (actionContext: { - target?: string; - frameButton: FrameButton; - buttonIndex: number; - url: string; - inputText?: string; - signer: SignerStorageType | null; - state?: string; - transactionId?: `0x${string}`; - address?: `0x${string}`; - frameContext: FrameContextType; - }) => Promise<{ - body: FrameActionBodyType; - searchParams: URLSearchParams; - }>; + signFrameAction: SignFrameActionFunction< + SignerStateActionContext, + TFrameActionBodyType + >; /** is loading the signer */ - isLoadingSigner?: boolean; + isLoadingSigner: boolean; /** A function called when a frame button is clicked without a signer */ - onSignerlessFramePress: () => void; - logout?: () => void; + onSignerlessFramePress: () => Promise; + logout: () => Promise; + withContext: ( + context: TFrameContextType, + overrides?: { + specification?: + | SupportedParsingSpecification + | SupportedParsingSpecification[]; + } + ) => { + signerState: SignerStateInstance< + TSignerStorageType, + TFrameActionBodyType, + TFrameContextType + >; + frameContext: TFrameContextType; + }; } -export type FrameRequest = +export type FrameGETRequest = { + method: "GET"; + url: string; +}; + +export type FramePOSTRequest< + TSignerStateActionContext extends SignerStateActionContext< + unknown, + Record + > = SignerStateActionContext, +> = | { - method: "GET"; - url: string; + method: "POST"; + source?: never; + frameButton: FrameButtonPost | FrameButtonTx; + signerStateActionContext: TSignerStateActionContext; + isDangerousSkipSigning: boolean; + /** + * The frame that was the source of the button press. + */ + sourceFrame: Frame; } | { method: "POST"; - request: { - body: object; - searchParams: URLSearchParams; - }; - url: string; + source: "cast-action" | "composer-action"; + frameButton: FrameButtonPost | FrameButtonTx; + signerStateActionContext: TSignerStateActionContext; + isDangerousSkipSigning: boolean; + sourceFrame: undefined; }; +export type FrameRequest< + TSignerStateActionContext extends SignerStateActionContext< + unknown, + Record + > = SignerStateActionContext, +> = FrameGETRequest | FramePOSTRequest; + export type FrameStackBase = { + id: number; timestamp: Date; /** speed in seconds */ speed: number; responseStatus: number; -} & FrameRequest; + responseBody: unknown; + requestDetails: { + body?: object; + searchParams?: URLSearchParams; + }; + url: string; +}; -export type FrameStackPending = { +export type FrameStackPostPending = { + id: number; + method: "POST"; timestamp: Date; status: "pending"; -} & FrameRequest; + request: FramePOSTRequest; + requestDetails: { + body?: object; + searchParams?: URLSearchParams; + }; + url: string; +}; + +export type FrameStackGetPending = { + id: number; + method: "GET"; + timestamp: Date; + status: "pending"; + request: FrameGETRequest; + requestDetails: { + body?: object; + searchParams?: URLSearchParams; + }; + url: string; +}; -type GetFrameResult = ReturnType; +export type FrameStackPending = FrameStackGetPending | FrameStackPostPending; + +export type GetFrameResult = Awaited>; export type FrameStackDone = FrameStackBase & { - frames: Record< - keyof GetFrameResult, - | { frame: Frame; status: "valid" } - | { - frame: Partial; - reports: Record; - status: "invalid"; - } - | { - frame: Partial; - reports: Record; - status: "warnings"; - } - >; + request: FrameRequest; + response: Response; + frameResult: GetFrameResult; status: "done"; }; +export type FrameStackDoneRedirect = FrameStackBase & { + request: FramePOSTRequest; + response: Response; + location: string; + status: "doneRedirect"; +}; + export type FrameStackRequestError = FrameStackBase & { + request: FrameRequest; + response: Response | null; status: "requestError"; - requestError: unknown; + requestError: Error; +}; + +export type FrameStackMessage = FrameStackBase & { + request: FramePOSTRequest; + response: Response; + status: "message"; + message: string; + type: "info" | "error"; }; export type FramesStackItem = | FrameStackPending | FrameStackDone - | FrameStackRequestError; + | FrameStackDoneRedirect + | FrameStackRequestError + | FrameStackMessage; export type FramesStack = FramesStackItem[]; -export type FrameState = { - fetchFrame: (request: FrameRequest) => void | Promise; +export type FrameReducerActions = + | { + action: "LOAD"; + item: FrameStackPending; + } + | { + action: "REQUEST_ERROR"; + pendingItem: FrameStackPending; + item: FrameStackRequestError; + } + | { + action: "DONE_REDIRECT"; + pendingItem: FrameStackPending; + item: FrameStackDoneRedirect; + } + | { + action: "DONE"; + pendingItem: FrameStackPending; + item: FramesStackItem; + } + | { action: "CLEAR" } + | { + action: "RESET_INITIAL_FRAME"; + resultOrFrame: ParseResult | Frame; + homeframeUrl: string | null | undefined; + specification: Exclude; + }; + +export type ButtonPressFunction< + TSignerStateActionContext extends SignerStateActionContext< + unknown, + Record + >, +> = ( + frame: Frame, + frameButton: FrameButton, + index: number, + fetchFrameOverride?: FetchFrameFunction +) => void | Promise; + +type CastActionButtonPressFunctionArg = { + castAction: CastActionResponse & { + /** URL to cast action handler */ + url: string; + }; + /** + * @defaultValue false + */ + clearStack?: boolean; +}; + +export type CastActionButtonPressFunction = ( + arg: CastActionButtonPressFunctionArg +) => Promise; + +type ComposerActionButtonPressFunctionArg = { + castAction: CastActionResponse & { + /** URL to cast action handler */ + url: string; + }; + composerActionState: ComposerActionState; + /** + * @defaultValue false + */ + clearStack?: boolean; +}; + +export type ComposerActionButtonPressFunction = ( + arg: ComposerActionButtonPressFunctionArg +) => Promise; + +export type CastActionRequest< + TSignerStateActionContext extends SignerStateActionContext< + unknown, + Record + > = SignerStateActionContext, +> = Omit< + FramePOSTRequest, + "method" | "frameButton" | "sourceFrame" | "signerStateActionContext" +> & { + method: "CAST_ACTION"; + action: CastActionResponse & { + url: string; + }; + signerStateActionContext: Omit< + FramePOSTRequest["signerStateActionContext"], + "frameButton" | "inputText" | "state" + >; +}; + +export type ComposerActionRequest< + TSignerStateActionContext extends SignerStateActionContext< + unknown, + Record + > = SignerStateActionContext, +> = Omit< + FramePOSTRequest, + "method" | "frameButton" | "sourceFrame" | "signerStateActionContext" +> & { + method: "COMPOSER_ACTION"; + action: CastActionResponse & { + url: string; + }; + composerActionState: ComposerActionState; + signerStateActionContext: Omit< + FramePOSTRequest["signerStateActionContext"], + "frameButton" | "inputText" | "state" + >; +}; + +export type FetchFrameFunction< + TSignerStateActionContext extends SignerStateActionContext< + unknown, + Record + > = SignerStateActionContext, +> = ( + request: + | FrameRequest + | CastActionRequest + | ComposerActionRequest, + /** + * If true, the frame stack will be cleared before the new frame is loaded + * + * @defaultValue false + */ + shouldClear?: boolean +) => Promise; + +export type FrameState< + TSignerStorageType = Record, + TFrameContextType extends FrameContext = FrameContext, +> = { + fetchFrame: FetchFrameFunction< + SignerStateActionContext + >; clearFrameStack: () => void; + dispatchFrameStack: Dispatch; /** The frame at the top of the stack (at index 0) */ - frame: FramesStackItem | undefined; + currentFrameStackItem: FramesStackItem | undefined; /** A stack of frames with additional context, with the most recent frame at index 0 */ framesStack: FramesStack; inputText: string; setInputText: (s: string) => void; - onButtonPress: ( - frame: Frame, - frameButton: FrameButton, - index: number - ) => void | Promise; + onButtonPress: ButtonPressFunction< + SignerStateActionContext + >; homeframeUrl: string | null | undefined; + onCastActionButtonPress: CastActionButtonPressFunction; + onComposerActionButtonPress: ComposerActionButtonPressFunction; }; export type OnMintArgs = { @@ -173,12 +700,6 @@ export type OnMintArgs = { frame: Frame; }; -export type OnTransactionArgs = { - transactionData: TransactionTargetResponse; - frameButton: FrameButton; - frame: Frame; -}; - export const themeParams = [ "bg", "buttonColor", @@ -190,7 +711,8 @@ export const themeParams = [ export type FrameTheme = Partial>; +// @TODO define minimal action body payload shape, because it is mostly the same export type FrameActionBodyPayload = Record; -export type FrameContext = FarcasterFrameContext; +export type FrameContext = Record; ```