Skip to content

Commit 234e4d0

Browse files
feat: simpler frame app context resolving (#542)
1 parent 104b0d8 commit 234e4d0

File tree

6 files changed

+122
-119
lines changed

6 files changed

+122
-119
lines changed

.changeset/fuzzy-cherries-mate.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@frames.js/debugger": patch
3+
"@frames.js/render": patch
4+
---
5+
6+
feat: simpler frame app context resolution

packages/debugger/app/components/frame-app.tsx

+64-51
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import { useConfig } from "wagmi";
1111
import type { EIP6963ProviderInfo } from "@farcaster/frame-sdk";
1212
import type {
1313
FramePrimaryButton,
14-
ResolveClientFunction,
14+
ResolveContextFunction,
15+
FrameContext,
1516
} from "@frames.js/render/frame-app/types";
1617
import { useCallback, useEffect, useRef, useState } from "react";
1718
import type { UseQueryResult } from "@tanstack/react-query";
@@ -84,67 +85,79 @@ export function FrameApp({
8485
const frameAppNotificationManagerPromiseRef = useRef(
8586
frameAppNotificationManager.promise
8687
);
87-
const resolveClient: ResolveClientFunction = useCallback(async () => {
88-
try {
89-
const clientInfoResponse = await fetch("/client-info");
88+
const resolveContext: ResolveContextFunction = useCallback(
89+
async ({ signal }) => {
90+
const location: FrameContext["location"] =
91+
context.context === "button_press"
92+
? {
93+
type: "launcher",
94+
}
95+
: {
96+
type: "cast_embed",
97+
embed: "",
98+
cast: fallbackFrameContext.castId,
99+
};
90100

91-
if (!clientInfoResponse.ok) {
92-
throw new Error("Failed to fetch client info");
93-
}
101+
try {
102+
const clientInfoResponse = await fetch("/client-info", {
103+
signal,
104+
});
94105

95-
const parseClientInfo = z.object({
96-
fid: z.number().int(),
97-
});
106+
if (!clientInfoResponse.ok) {
107+
throw new Error("Failed to fetch client info");
108+
}
98109

99-
const clientInfo = parseClientInfo.parse(await clientInfoResponse.json());
110+
const parseClientInfo = z.object({
111+
fid: z.number().int(),
112+
});
100113

101-
const { manager } = await frameAppNotificationManagerPromiseRef.current;
102-
const clientFid = clientInfo.fid;
114+
const clientInfo = parseClientInfo.parse(
115+
await clientInfoResponse.json()
116+
);
117+
118+
const { manager } = await frameAppNotificationManagerPromiseRef.current;
119+
const clientFid = clientInfo.fid;
103120

104-
if (!manager.state || manager.state.frame.status === "removed") {
105121
return {
106-
clientFid,
107-
added: false,
122+
client: {
123+
clientFid,
124+
added: manager.state?.frame.status === "added",
125+
notificationDetails:
126+
manager.state?.frame.status === "added"
127+
? manager.state.frame.notificationDetails ?? undefined
128+
: undefined,
129+
},
130+
location,
131+
user: userContext,
108132
};
109-
}
110-
111-
return {
112-
clientFid,
113-
added: true,
114-
notificationDetails:
115-
manager.state.frame.notificationDetails ?? undefined,
116-
};
117-
} catch (e) {
118-
console.error(e);
119-
120-
toast({
121-
title: "Unexpected error",
122-
description:
123-
"Failed to load notifications settings. Check the console for more details.",
124-
variant: "destructive",
125-
});
133+
} catch (e) {
134+
if (!(typeof e === "string" && e.startsWith("Aborted because"))) {
135+
console.error(e);
136+
137+
toast({
138+
title: "Unexpected error",
139+
description:
140+
"Failed to load notifications settings. Check the console for more details.",
141+
variant: "destructive",
142+
});
143+
}
126144

127-
return {
128-
clientFid: -1,
129-
added: false,
130-
};
131-
}
132-
}, [toast]);
145+
return {
146+
client: {
147+
clientFid: -1,
148+
added: false,
149+
},
150+
location,
151+
user: userContext,
152+
};
153+
}
154+
},
155+
[toast, context, userContext]
156+
);
133157
const frameApp = useFrameAppInIframe({
134158
debug: true,
135159
source: context.parseResult,
136-
client: resolveClient,
137-
location:
138-
context.context === "button_press"
139-
? {
140-
type: "launcher",
141-
}
142-
: {
143-
type: "cast_embed",
144-
embed: "",
145-
cast: fallbackFrameContext.castId,
146-
},
147-
user: userContext,
160+
context: resolveContext,
148161
provider,
149162
proxyUrl: "/frames",
150163
addFrameRequestsCache,

packages/debugger/app/notifications/types.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type {
22
FrameNotificationDetails,
33
SendNotificationRequest,
44
} from "@farcaster/frame-sdk";
5-
import { FrameClientConfig } from "@frames.js/render/frame-app/types";
65
import type { FrameServerEvent } from "frames.js/farcaster-v2/events";
76

87
export type Notification = SendNotificationRequest;
@@ -40,7 +39,7 @@ export type RecordedEvent =
4039
export type NotificationSettings =
4140
| {
4241
enabled: true;
43-
details: NonNullable<FrameClientConfig["notificationDetails"]>;
42+
details: FrameNotificationDetails;
4443
webhookUrl: string;
4544
signerPrivateKey: string;
4645
}

packages/render/src/frame-app/types.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-pa
1010
import type { Provider } from "ox/Provider";
1111
import type { Default as DefaultRpcSchema, ExtractRequest } from "ox/RpcSchema";
1212

13-
export type FrameClientConfig = Context.ClientContext;
14-
1513
export type SendTransactionRpcRequest = ExtractRequest<
1614
DefaultRpcSchema,
1715
"eth_sendTransaction"
@@ -99,12 +97,21 @@ export type OnSignInFunction = (
9997

10098
export type OnViewProfileFunction = FrameHost["viewProfile"];
10199

100+
export type FrameContext = Context.FrameContext;
101+
102+
export type ResolveContextFunctionOptions = {
103+
/**
104+
* Called when hook is unmounted
105+
*/
106+
signal: AbortSignal;
107+
};
108+
102109
/**
103-
* Function called when the frame app is being loaded and we need to resolve the client that renders the frame app
110+
* Function called when the frame app is loaded and needs a context to be rendered
104111
*/
105-
export type ResolveClientFunction = (options: {
106-
signal: AbortSignal;
107-
}) => Promise<FrameClientConfig>;
112+
export type ResolveContextFunction = (
113+
options: ResolveContextFunctionOptions
114+
) => Promise<FrameContext>;
108115

109116
export type HostEndpointEmitter = Pick<
110117
HostEndpoint,
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
2-
import type { FrameClientConfig, ResolveClientFunction } from "./types";
2+
import type { ResolveContextFunction, FrameContext } from "./types";
33

4-
type UseResolveClientOptions = {
5-
client: FrameClientConfig | ResolveClientFunction;
4+
type UseResolveContextOptions = {
5+
context: FrameContext | ResolveContextFunction;
66
};
77

8-
type UseResolveClientResult =
8+
type UseResolveContextResult =
99
| {
1010
status: "success";
11-
client: FrameClientConfig;
11+
context: FrameContext;
1212
}
1313
| {
1414
status: "error";
@@ -18,15 +18,15 @@ type UseResolveClientResult =
1818
status: "pending";
1919
};
2020

21-
export function useResolveClient({
22-
client,
23-
}: UseResolveClientOptions): UseResolveClientResult {
21+
export function useResolveContext({
22+
context,
23+
}: UseResolveContextOptions): UseResolveContextResult {
2424
const abortControllerRef = useRef<AbortController | null>(null);
25-
const [state, setState] = useState<UseResolveClientResult>(() => {
26-
if (typeof client !== "function") {
25+
const [state, setState] = useState<UseResolveContextResult>(() => {
26+
if (typeof context !== "function") {
2727
return {
2828
status: "success",
29-
client,
29+
context,
3030
};
3131
}
3232

@@ -35,7 +35,7 @@ export function useResolveClient({
3535
};
3636
});
3737

38-
const resolveClient = useCallback((resolve: ResolveClientFunction) => {
38+
const resolveContext = useCallback((resolve: ResolveContextFunction) => {
3939
// cancel previous request
4040
abortControllerRef.current?.abort();
4141

@@ -47,7 +47,7 @@ export function useResolveClient({
4747
setState({
4848
status: "pending",
4949
});
50-
const resolvedClient = await resolve({
50+
const resolvedContext = await resolve({
5151
signal: abortController.signal,
5252
});
5353

@@ -57,7 +57,7 @@ export function useResolveClient({
5757

5858
setState({
5959
status: "success",
60-
client: resolvedClient,
60+
context: resolvedContext,
6161
});
6262
})
6363
.catch((e) => {
@@ -72,26 +72,28 @@ export function useResolveClient({
7272
});
7373

7474
return () => {
75-
abortController.abort();
75+
abortController.abort(
76+
"Aborted because the component has been unmounted/remounted"
77+
);
7678
};
7779
}, []);
7880

7981
useEffect(() => {
80-
if (typeof client !== "function") {
82+
if (typeof context !== "function") {
8183
setState((prevState) => {
82-
if (prevState.status === "success" && prevState.client !== client) {
84+
if (prevState.status === "success" && prevState.context !== context) {
8385
return {
8486
status: "success",
85-
client,
87+
context,
8688
};
8789
}
8890

8991
return prevState;
9092
});
9193
} else {
92-
resolveClient(client);
94+
return resolveContext(context);
9395
}
94-
}, [client, resolveClient]);
96+
}, [context, resolveContext]);
9597

9698
return state;
9799
}

0 commit comments

Comments
 (0)