diff --git a/.changeset/chilled-spies-vanish.md b/.changeset/chilled-spies-vanish.md new file mode 100644 index 000000000..a63e480ff --- /dev/null +++ b/.changeset/chilled-spies-vanish.md @@ -0,0 +1,5 @@ +--- +"@frames.js/render": patch +--- + +feat: add onMissingSigner callback to useFrame_unstable diff --git a/.changeset/clean-seas-sort.md b/.changeset/clean-seas-sort.md new file mode 100644 index 000000000..a9f0b1b08 --- /dev/null +++ b/.changeset/clean-seas-sort.md @@ -0,0 +1,6 @@ +--- +"@frames.js/render": patch +"docs": patch +--- + +feat: expose onSignature callbacks for eth_signTypedData request from server diff --git a/.changeset/strange-oranges-count.md b/.changeset/strange-oranges-count.md new file mode 100644 index 000000000..4aa4cddf8 --- /dev/null +++ b/.changeset/strange-oranges-count.md @@ -0,0 +1,5 @@ +--- +"@frames.js/render": patch +--- + +feat: expose onSignature callbacks for eth_signTypedData request from server diff --git a/docs/pages/reference/render/types.mdx b/docs/pages/reference/render/types.mdx index 85df5fe03..d01899302 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; ``` diff --git a/docs/pages/reference/render/use-frame.mdx b/docs/pages/reference/render/use-frame.mdx index 047785222..def1d30fe 100644 --- a/docs/pages/reference/render/use-frame.mdx +++ b/docs/pages/reference/render/use-frame.mdx @@ -88,6 +88,12 @@ A function to handle redirect responses from the frame. This happens when you cl A function to handle transaction button presses, returns the transaction hash or null. The function is called when a user presses a transaction button and endpoint specified by `target` returns transaction data. +### `onSignature` + +- Type: `OnSignatureFunc` + +A function to handle transaction button presses, returns the signature or null. The function is called when a user presses a transaction button and endpoint specified by `target` returns typed data for signing (`eth_signTypedData_v4`). + ### `transactionDataSuffix` - Type: `0x${string}` diff --git a/package.json b/package.json index 2ff298f1f..5713b06c5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dev:utils-starter": "FJS_MONOREPO=true turbo dev --filter=template-next-utils-starter... --filter=debugger...", "lint": "turbo lint --filter=!template-*", "test:ci": "jest --ci", - "test": "cd ./packages/frames.js && npm run test:watch", + "test": "cd ./packages/frames.js && yarn test:watch", "check:package-types": "turbo check:package-types", "check:package-lint": "turbo check:package-lint", "check:types": "turbo check:types", diff --git a/packages/render/src/types.ts b/packages/render/src/types.ts index dd798f736..864996ec5 100644 --- a/packages/render/src/types.ts +++ b/packages/render/src/types.ts @@ -43,6 +43,8 @@ export type OnSignatureFunc = ( args: OnSignatureArgs ) => Promise<`0x${string}` | null>; +export type OnMissingSignerFunction = () => void; + type OnComposerFormActionFuncArgs = { form: ComposerActionFormResponse; cast: ComposerActionState; @@ -126,6 +128,9 @@ export type UseFetchFrameOptions< 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; /** @@ -281,6 +286,9 @@ export type UseFrameOptions< | "onTransactionError" | "onTransactionStart" | "onTransactionSuccess" + | "onSignatureError" + | "onSignatureStart" + | "onSignatureSuccess" | "onTransactionProcessingError" | "onTransactionProcessingStart" | "onTransactionProcessingSuccess" diff --git a/packages/render/src/unstable-types.ts b/packages/render/src/unstable-types.ts index a65d7c1c4..868504764 100644 --- a/packages/render/src/unstable-types.ts +++ b/packages/render/src/unstable-types.ts @@ -26,6 +26,7 @@ import type { FrameGETRequest, FramePOSTRequest, FrameRequest, + OnMissingSignerFunction, OnMintArgs, OnSignatureFunc, OnTransactionFunc, @@ -200,6 +201,8 @@ export type UseFrameOptions< * Only for frames v2 */ onLaunchFrameButtonPressed?: LaunchFrameButtonPressFunction; + + onMissingSigner?: OnMissingSignerFunction; } & Partial< Pick< UseFetchFrameOptions, @@ -211,6 +214,9 @@ export type UseFrameOptions< | "onTransactionError" | "onTransactionStart" | "onTransactionSuccess" + | "onSignatureError" + | "onSignatureStart" + | "onSignatureSuccess" | "onTransactionProcessingError" | "onTransactionProcessingStart" | "onTransactionProcessingSuccess" @@ -515,6 +521,9 @@ export type UseFetchFrameOptions< onTransaction: OnTransactionFunction; /** 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: OnSignatureFunction; /** * This function can be used to customize how error is reported to the user. diff --git a/packages/render/src/unstable-use-frame.tsx b/packages/render/src/unstable-use-frame.tsx index 6d4a4592e..5703922e5 100644 --- a/packages/render/src/unstable-use-frame.tsx +++ b/packages/render/src/unstable-use-frame.tsx @@ -19,7 +19,7 @@ import type { import { useFrameState } from "./unstable-use-frame-state"; import { useFetchFrame } from "./unstable-use-fetch-frame"; import { useFreshRef } from "./hooks/use-fresh-ref"; -import { tryCallAsync } from "./helpers"; +import { tryCall, tryCallAsync } from "./helpers"; function onErrorFallback(e: Error): void { console.error("@frames.js/render", e); @@ -186,9 +186,13 @@ export function useFrame_unstable< onTransactionError, onTransactionStart, onTransactionSuccess, + onSignatureError, + onSignatureStart, + onSignatureSuccess, onTransactionProcessingError, onTransactionProcessingStart, onTransactionProcessingSuccess, + onMissingSigner, }: UseFrameOptions< TExtraDataPending, TExtraDataDone, @@ -235,6 +239,9 @@ export function useFrame_unstable< onTransactionError, onTransactionStart, onTransactionSuccess, + onSignatureError, + onSignatureStart, + onSignatureSuccess, onTransactionProcessingError, onTransactionProcessingStart, onTransactionProcessingSuccess, @@ -243,6 +250,7 @@ export function useFrame_unstable< const fetchFrameRef = useFreshRef(fetchFrame); const onErrorRef = useFreshRef(onError); + const onMissingSignerRef = useFreshRef(onMissingSigner); useEffect(() => { if (!homeframeUrl) { @@ -312,6 +320,7 @@ export function useFrame_unstable< } if (!currentState.signerState.hasSigner) { + tryCall(() => onMissingSignerRef.current?.()); await currentState.signerState.onSignerlessFramePress(); return; } @@ -335,7 +344,7 @@ export function useFrame_unstable< sourceFrame: currentFrame, }); }, - [fetchFrameRef, frameStateRef, onErrorRef] + [fetchFrameRef, frameStateRef, onErrorRef, onMissingSignerRef] ); const resolveAddressRef = useFreshRef(resolveAddress); @@ -364,6 +373,7 @@ export function useFrame_unstable< // Send post request to get calldata if (!currentState.signerState.hasSigner) { + tryCall(() => onMissingSignerRef.current?.()); await currentState.signerState.onSignerlessFramePress(); return; } @@ -411,7 +421,7 @@ export function useFrame_unstable< sourceFrame: currentFrame, }); }, - [frameStateRef, fetchFrameRef, onErrorRef, resolveAddressRef] + [frameStateRef, fetchFrameRef, onErrorRef, onMissingSignerRef, resolveAddressRef] ); const onLaunchFrameButtonPressRef = useFreshRef(onLaunchFrameButtonPressed); diff --git a/packages/render/src/use-frame.tsx b/packages/render/src/use-frame.tsx index dedafe433..10273ea3d 100644 --- a/packages/render/src/use-frame.tsx +++ b/packages/render/src/use-frame.tsx @@ -183,11 +183,14 @@ export function useFrame< onTransactionDataStart, onTransactionDataSuccess, onTransactionError, + onTransactionStart, + onTransactionSuccess, + onSignatureError, + onSignatureStart, + onSignatureSuccess, onTransactionProcessingError, onTransactionProcessingStart, onTransactionProcessingSuccess, - onTransactionStart, - onTransactionSuccess, }: UseFrameOptions< TSignerStorageType, TFrameActionBodyType, @@ -231,11 +234,14 @@ export function useFrame< onTransactionDataStart, onTransactionDataSuccess, onTransactionError, + onTransactionStart, + onTransactionSuccess, + onSignatureError, + onSignatureStart, + onSignatureSuccess, onTransactionProcessingError, onTransactionProcessingStart, onTransactionProcessingSuccess, - onTransactionStart, - onTransactionSuccess, }); const fetchFrameRef = useFreshRef(fetchFrame); @@ -325,6 +331,7 @@ export function useFrame< homeframeUrl, signerState.hasSigner, signerState.signer, + onErrorRef, ] ); @@ -395,6 +402,7 @@ export function useFrame< connectedAddress, homeframeUrl, signerState, + onErrorRef, ] ); @@ -516,6 +524,7 @@ export function useFrame< onPostButton, onTransactionButton, signerState, + onErrorRef, ] );