Skip to content

Commit

Permalink
feat: simpler frame app context resolving (#542)
Browse files Browse the repository at this point in the history
  • Loading branch information
michalkvasnicak authored Jan 30, 2025
1 parent 104b0d8 commit 234e4d0
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 119 deletions.
6 changes: 6 additions & 0 deletions .changeset/fuzzy-cherries-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@frames.js/debugger": patch
"@frames.js/render": patch
---

feat: simpler frame app context resolution
115 changes: 64 additions & 51 deletions packages/debugger/app/components/frame-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { useConfig } from "wagmi";
import type { EIP6963ProviderInfo } from "@farcaster/frame-sdk";
import type {
FramePrimaryButton,
ResolveClientFunction,
ResolveContextFunction,
FrameContext,
} from "@frames.js/render/frame-app/types";
import { useCallback, useEffect, useRef, useState } from "react";
import type { UseQueryResult } from "@tanstack/react-query";
Expand Down Expand Up @@ -84,67 +85,79 @@ export function FrameApp({
const frameAppNotificationManagerPromiseRef = useRef(
frameAppNotificationManager.promise
);
const resolveClient: ResolveClientFunction = useCallback(async () => {
try {
const clientInfoResponse = await fetch("/client-info");
const resolveContext: ResolveContextFunction = useCallback(
async ({ signal }) => {
const location: FrameContext["location"] =
context.context === "button_press"
? {
type: "launcher",
}
: {
type: "cast_embed",
embed: "",
cast: fallbackFrameContext.castId,
};

if (!clientInfoResponse.ok) {
throw new Error("Failed to fetch client info");
}
try {
const clientInfoResponse = await fetch("/client-info", {
signal,
});

const parseClientInfo = z.object({
fid: z.number().int(),
});
if (!clientInfoResponse.ok) {
throw new Error("Failed to fetch client info");
}

const clientInfo = parseClientInfo.parse(await clientInfoResponse.json());
const parseClientInfo = z.object({
fid: z.number().int(),
});

const { manager } = await frameAppNotificationManagerPromiseRef.current;
const clientFid = clientInfo.fid;
const clientInfo = parseClientInfo.parse(
await clientInfoResponse.json()
);

const { manager } = await frameAppNotificationManagerPromiseRef.current;
const clientFid = clientInfo.fid;

if (!manager.state || manager.state.frame.status === "removed") {
return {
clientFid,
added: false,
client: {
clientFid,
added: manager.state?.frame.status === "added",
notificationDetails:
manager.state?.frame.status === "added"
? manager.state.frame.notificationDetails ?? undefined
: undefined,
},
location,
user: userContext,
};
}

return {
clientFid,
added: true,
notificationDetails:
manager.state.frame.notificationDetails ?? undefined,
};
} catch (e) {
console.error(e);

toast({
title: "Unexpected error",
description:
"Failed to load notifications settings. Check the console for more details.",
variant: "destructive",
});
} catch (e) {
if (!(typeof e === "string" && e.startsWith("Aborted because"))) {
console.error(e);

toast({
title: "Unexpected error",
description:
"Failed to load notifications settings. Check the console for more details.",
variant: "destructive",
});
}

return {
clientFid: -1,
added: false,
};
}
}, [toast]);
return {
client: {
clientFid: -1,
added: false,
},
location,
user: userContext,
};
}
},
[toast, context, userContext]
);
const frameApp = useFrameAppInIframe({
debug: true,
source: context.parseResult,
client: resolveClient,
location:
context.context === "button_press"
? {
type: "launcher",
}
: {
type: "cast_embed",
embed: "",
cast: fallbackFrameContext.castId,
},
user: userContext,
context: resolveContext,
provider,
proxyUrl: "/frames",
addFrameRequestsCache,
Expand Down
3 changes: 1 addition & 2 deletions packages/debugger/app/notifications/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {
FrameNotificationDetails,
SendNotificationRequest,
} from "@farcaster/frame-sdk";
import { FrameClientConfig } from "@frames.js/render/frame-app/types";
import type { FrameServerEvent } from "frames.js/farcaster-v2/events";

export type Notification = SendNotificationRequest;
Expand Down Expand Up @@ -40,7 +39,7 @@ export type RecordedEvent =
export type NotificationSettings =
| {
enabled: true;
details: NonNullable<FrameClientConfig["notificationDetails"]>;
details: FrameNotificationDetails;
webhookUrl: string;
signerPrivateKey: string;
}
Expand Down
19 changes: 13 additions & 6 deletions packages/render/src/frame-app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-pa
import type { Provider } from "ox/Provider";
import type { Default as DefaultRpcSchema, ExtractRequest } from "ox/RpcSchema";

export type FrameClientConfig = Context.ClientContext;

export type SendTransactionRpcRequest = ExtractRequest<
DefaultRpcSchema,
"eth_sendTransaction"
Expand Down Expand Up @@ -99,12 +97,21 @@ export type OnSignInFunction = (

export type OnViewProfileFunction = FrameHost["viewProfile"];

export type FrameContext = Context.FrameContext;

export type ResolveContextFunctionOptions = {
/**
* Called when hook is unmounted
*/
signal: AbortSignal;
};

/**
* Function called when the frame app is being loaded and we need to resolve the client that renders the frame app
* Function called when the frame app is loaded and needs a context to be rendered
*/
export type ResolveClientFunction = (options: {
signal: AbortSignal;
}) => Promise<FrameClientConfig>;
export type ResolveContextFunction = (
options: ResolveContextFunctionOptions
) => Promise<FrameContext>;

export type HostEndpointEmitter = Pick<
HostEndpoint,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { FrameClientConfig, ResolveClientFunction } from "./types";
import type { ResolveContextFunction, FrameContext } from "./types";

type UseResolveClientOptions = {
client: FrameClientConfig | ResolveClientFunction;
type UseResolveContextOptions = {
context: FrameContext | ResolveContextFunction;
};

type UseResolveClientResult =
type UseResolveContextResult =
| {
status: "success";
client: FrameClientConfig;
context: FrameContext;
}
| {
status: "error";
Expand All @@ -18,15 +18,15 @@ type UseResolveClientResult =
status: "pending";
};

export function useResolveClient({
client,
}: UseResolveClientOptions): UseResolveClientResult {
export function useResolveContext({
context,
}: UseResolveContextOptions): UseResolveContextResult {
const abortControllerRef = useRef<AbortController | null>(null);
const [state, setState] = useState<UseResolveClientResult>(() => {
if (typeof client !== "function") {
const [state, setState] = useState<UseResolveContextResult>(() => {
if (typeof context !== "function") {
return {
status: "success",
client,
context,
};
}

Expand All @@ -35,7 +35,7 @@ export function useResolveClient({
};
});

const resolveClient = useCallback((resolve: ResolveClientFunction) => {
const resolveContext = useCallback((resolve: ResolveContextFunction) => {
// cancel previous request
abortControllerRef.current?.abort();

Expand All @@ -47,7 +47,7 @@ export function useResolveClient({
setState({
status: "pending",
});
const resolvedClient = await resolve({
const resolvedContext = await resolve({
signal: abortController.signal,
});

Expand All @@ -57,7 +57,7 @@ export function useResolveClient({

setState({
status: "success",
client: resolvedClient,
context: resolvedContext,
});
})
.catch((e) => {
Expand All @@ -72,26 +72,28 @@ export function useResolveClient({
});

return () => {
abortController.abort();
abortController.abort(
"Aborted because the component has been unmounted/remounted"
);
};
}, []);

useEffect(() => {
if (typeof client !== "function") {
if (typeof context !== "function") {
setState((prevState) => {
if (prevState.status === "success" && prevState.client !== client) {
if (prevState.status === "success" && prevState.context !== context) {
return {
status: "success",
client,
context,
};
}

return prevState;
});
} else {
resolveClient(client);
return resolveContext(context);
}
}, [client, resolveClient]);
}, [context, resolveContext]);

return state;
}
Loading

0 comments on commit 234e4d0

Please sign in to comment.