Skip to content

Commit 344fc74

Browse files
fix: unregister exposed comlink listeners
1 parent 060b8b4 commit 344fc74

File tree

4 files changed

+88
-21
lines changed

4 files changed

+88
-21
lines changed

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

+7-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { FarcasterMultiSignerInstance } from "@frames.js/render/identity/fa
1717
import { Loader2Icon } from "lucide-react";
1818
import { useWalletClient } from "wagmi";
1919
import { Button } from "@/components/ui/button";
20+
import { useToast } from "@/components/ui/use-toast";
2021

2122
type FrameAppDialogProps = {
2223
farcasterSigner: FarcasterMultiSignerInstance;
@@ -32,6 +33,7 @@ export function FrameAppDialog({
3233
frameState,
3334
onClose,
3435
}: FrameAppDialogProps) {
36+
const { toast } = useToast();
3537
const walletClient = useWalletClient();
3638
const [isReady, setIsReady] = useState(false);
3739
const [primaryButton, setPrimaryButton] = useState<FramePrimaryButton | null>(
@@ -50,7 +52,6 @@ export function FrameAppDialog({
5052
},
5153
onPrimaryButtonSet: setPrimaryButton,
5254
});
53-
const iframeRef = useRef<HTMLIFrameElement>(null);
5455
const { name, url, splashImageUrl, splashBackgroundColor } =
5556
frameState.frame.button.action;
5657

@@ -93,7 +94,6 @@ export function FrameAppDialog({
9394
)}
9495
<iframe
9596
className="h-[600px] w-full opacity-100 transition-opacity duration-300"
96-
ref={iframeRef}
9797
onLoad={frameApp.onLoad}
9898
src={url}
9999
sandbox="allow-forms allow-scripts allow-same-origin"
@@ -105,13 +105,11 @@ export function FrameAppDialog({
105105
className="w-full m-1"
106106
disabled={primaryButton.disabled || primaryButton.loading}
107107
onClick={() => {
108-
iframeRef.current?.contentWindow?.dispatchEvent(
109-
new MessageEvent("FarcasterFrameEvent", {
110-
data: {
111-
type: "primaryButtonClicked",
112-
},
113-
})
114-
);
108+
toast({
109+
title: "Feature not implemented",
110+
description: "This feature is not implemented yet.",
111+
variant: "destructive",
112+
});
115113
}}
116114
size="lg"
117115
type="button"

packages/render/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@
327327
"dependencies": {
328328
"@farcaster/frame-sdk": "^0.0.5",
329329
"@farcaster/core": "^0.14.7",
330+
"@michalkvasnicak/comlink": "^4.5.0",
330331
"@noble/ed25519": "^2.0.0",
331-
"comlink": "^4.4.2",
332332
"frames.js": "^0.20.0",
333333
"zod": "^3.23.8"
334334
}

packages/render/src/unstable-use-frame-app.ts

+75-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { FrameV2 } from "frames.js";
2-
import { expose, windowEndpoint, type Endpoint } from "comlink";
3-
import { useCallback, useMemo } from "react";
2+
import {
3+
expose,
4+
windowEndpoint,
5+
type Endpoint,
6+
} from "@michalkvasnicak/comlink";
7+
import { useCallback, useEffect, useMemo, useRef } from "react";
48
import type { FrameHost, SetPrimaryButton } from "@farcaster/frame-sdk";
59
import type { UseWalletClientReturnType } from "wagmi";
610
import { useFreshRef } from "./hooks/use-fresh-ref";
@@ -53,7 +57,11 @@ type UseFrameAppOptions = {
5357
onPrimaryButtonSet?: SetPrimaryButton;
5458
};
5559

56-
type RegisterEndpointFunction = (endpoint: Endpoint) => void;
60+
type UnregisterEndpointFunction = () => void;
61+
62+
type RegisterEndpointFunction = (
63+
endpoint: Endpoint
64+
) => UnregisterEndpointFunction;
5765

5866
type UseFrameAppReturn = {
5967
/**
@@ -82,20 +90,29 @@ export function useFrameApp({
8290
const onSignerNotApprovedRef = useFreshRef(onSignerNotApproved);
8391
const onPrimaryButtonSetRef = useFreshRef(onPrimaryButtonSet);
8492
const farcasterSignerRef = useFreshRef(farcasterSigner);
93+
/**
94+
* Used to unregister message listener of previously exposed endpoint.
95+
*/
96+
const unregisterPreviouslyExposedEndpointListenerRef =
97+
useRef<UnregisterEndpointFunction>(() => {
98+
// no-op
99+
});
85100

86-
// @todo solve expose isolation per endpoint because it's not possible to clean up the exposed API at the moment unless the target releases its proxy
87-
// @see https://github.com/GoogleChromeLabs/comlink/issues/674
88-
// Perhaps this hook should be global and only once per whole app for now?
89101
const registerEndpoint = useCallback<RegisterEndpointFunction>(
90102
(endpoint) => {
103+
unregisterPreviouslyExposedEndpointListenerRef.current();
104+
91105
const signer = farcasterSignerRef.current.signer;
92106

93107
if (signer?.status !== "approved") {
94108
onSignerNotApprovedRef.current();
95-
return;
109+
110+
return () => {
111+
// no-op
112+
};
96113
}
97114

98-
expose(
115+
unregisterPreviouslyExposedEndpointListenerRef.current = expose(
99116
{
100117
close() {
101118
const handler = closeRef.current;
@@ -157,6 +174,8 @@ export function useFrameApp({
157174
endpoint,
158175
[new URL(frame.button.action.url).origin]
159176
);
177+
178+
return unregisterPreviouslyExposedEndpointListenerRef.current;
160179
},
161180
[
162181
clientRef,
@@ -179,21 +198,66 @@ type UseFrameAppInIframeReturn = {
179198
onLoad: (event: React.SyntheticEvent<HTMLIFrameElement>) => void;
180199
};
181200

201+
/**
202+
* Handles frame app in iframe.
203+
*
204+
* On unmount it automatically unregisters the endpoint listener.
205+
*
206+
* @example
207+
* ```
208+
* import { useFrameAppInIframe } from '@frames.js/render/unstable-use-frame-app';
209+
* import { useWalletClient } from 'wagmi';
210+
* import { useFarcasterSigner } from '@frames.js/render/identity/farcaster';
211+
*
212+
* function MyAppDialog() {
213+
* const walletClient = useWalletClient();
214+
* const farcasterSigner = useFarcasterSigner({
215+
* // ...
216+
* });
217+
* const frameApp = useFrameAppInIframe({
218+
* walletClient,
219+
* farcasterSigner,
220+
* // frame returned by useFrame() hook
221+
* frame: frameState.frame,
222+
* // ... handlers for frame app actions
223+
* });
224+
*
225+
* return <iframe ref={frameApp.ref} />;
226+
* }
227+
* ```
228+
*/
182229
export function useFrameAppInIframe(
183230
options: UseFrameAppOptions
184231
): UseFrameAppInIframeReturn {
185232
const frameApp = useFrameApp(options);
233+
const unregisterEndpointRef = useRef<UnregisterEndpointFunction>(() => {
234+
// no-op
235+
});
236+
237+
useEffect(() => {
238+
return () => {
239+
unregisterEndpointRef.current();
240+
};
241+
}, []);
186242

187243
return useMemo(() => {
188244
return {
189245
onLoad(event) {
246+
if (!(event.currentTarget instanceof HTMLIFrameElement)) {
247+
// eslint-disable-next-line no-console -- provide feedback to the developer
248+
console.error(
249+
'@frames.js/render/unstable-use-frame-app: "onLoad" called but event target is not an iframe'
250+
);
251+
252+
return;
253+
}
190254
if (!event.currentTarget.contentWindow) {
191255
return;
192256
}
193257

194-
frameApp.registerEndpoint(
195-
windowEndpoint(event.currentTarget.contentWindow)
196-
);
258+
const endpoint = windowEndpoint(event.currentTarget.contentWindow);
259+
260+
unregisterEndpointRef.current = frameApp.registerEndpoint(endpoint);
197261
},
198262
};
199263
}, [frameApp]);

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3393,6 +3393,11 @@
33933393
superstruct "^1.0.3"
33943394
uuid "^9.0.1"
33953395

3396+
"@michalkvasnicak/comlink@^4.5.0":
3397+
version "4.5.0"
3398+
resolved "https://registry.yarnpkg.com/@michalkvasnicak/comlink/-/comlink-4.5.0.tgz#6fd32a5383c6f925015851523d9757498281eb06"
3399+
integrity sha512-Vi81JHsOG/y8KCpscKnwlVuSt2Vu+xo6J3d59H6geFfj1sJ/BGyg6iosQDGmzPNNZmsXH2tVEkgYlHkAtA5Y9w==
3400+
33963401
"@microsoft/[email protected]":
33973402
version "0.16.2"
33983403
resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf"

0 commit comments

Comments
 (0)