Skip to content

Commit

Permalink
fix: unregister exposed comlink listeners
Browse files Browse the repository at this point in the history
  • Loading branch information
michalkvasnicak committed Dec 2, 2024
1 parent 060b8b4 commit 344fc74
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 21 deletions.
16 changes: 7 additions & 9 deletions packages/debugger/app/components/frame-app-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { FarcasterMultiSignerInstance } from "@frames.js/render/identity/fa
import { Loader2Icon } from "lucide-react";
import { useWalletClient } from "wagmi";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";

type FrameAppDialogProps = {
farcasterSigner: FarcasterMultiSignerInstance;
Expand All @@ -32,6 +33,7 @@ export function FrameAppDialog({
frameState,
onClose,
}: FrameAppDialogProps) {
const { toast } = useToast();
const walletClient = useWalletClient();
const [isReady, setIsReady] = useState(false);
const [primaryButton, setPrimaryButton] = useState<FramePrimaryButton | null>(
Expand All @@ -50,7 +52,6 @@ export function FrameAppDialog({
},
onPrimaryButtonSet: setPrimaryButton,
});
const iframeRef = useRef<HTMLIFrameElement>(null);
const { name, url, splashImageUrl, splashBackgroundColor } =
frameState.frame.button.action;

Expand Down Expand Up @@ -93,7 +94,6 @@ export function FrameAppDialog({
)}
<iframe
className="h-[600px] w-full opacity-100 transition-opacity duration-300"
ref={iframeRef}
onLoad={frameApp.onLoad}
src={url}
sandbox="allow-forms allow-scripts allow-same-origin"
Expand All @@ -105,13 +105,11 @@ export function FrameAppDialog({
className="w-full m-1"
disabled={primaryButton.disabled || primaryButton.loading}
onClick={() => {
iframeRef.current?.contentWindow?.dispatchEvent(
new MessageEvent("FarcasterFrameEvent", {
data: {
type: "primaryButtonClicked",
},
})
);
toast({
title: "Feature not implemented",
description: "This feature is not implemented yet.",
variant: "destructive",
});
}}
size="lg"
type="button"
Expand Down
2 changes: 1 addition & 1 deletion packages/render/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,8 @@
"dependencies": {
"@farcaster/frame-sdk": "^0.0.5",
"@farcaster/core": "^0.14.7",
"@michalkvasnicak/comlink": "^4.5.0",
"@noble/ed25519": "^2.0.0",
"comlink": "^4.4.2",
"frames.js": "^0.20.0",
"zod": "^3.23.8"
}
Expand Down
86 changes: 75 additions & 11 deletions packages/render/src/unstable-use-frame-app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { FrameV2 } from "frames.js";
import { expose, windowEndpoint, type Endpoint } from "comlink";
import { useCallback, useMemo } from "react";
import {
expose,
windowEndpoint,
type Endpoint,
} from "@michalkvasnicak/comlink";
import { useCallback, useEffect, useMemo, useRef } from "react";
import type { FrameHost, SetPrimaryButton } from "@farcaster/frame-sdk";
import type { UseWalletClientReturnType } from "wagmi";
import { useFreshRef } from "./hooks/use-fresh-ref";
Expand Down Expand Up @@ -53,7 +57,11 @@ type UseFrameAppOptions = {
onPrimaryButtonSet?: SetPrimaryButton;
};

type RegisterEndpointFunction = (endpoint: Endpoint) => void;
type UnregisterEndpointFunction = () => void;

type RegisterEndpointFunction = (
endpoint: Endpoint
) => UnregisterEndpointFunction;

type UseFrameAppReturn = {
/**
Expand Down Expand Up @@ -82,20 +90,29 @@ export function useFrameApp({
const onSignerNotApprovedRef = useFreshRef(onSignerNotApproved);
const onPrimaryButtonSetRef = useFreshRef(onPrimaryButtonSet);
const farcasterSignerRef = useFreshRef(farcasterSigner);
/**
* Used to unregister message listener of previously exposed endpoint.
*/
const unregisterPreviouslyExposedEndpointListenerRef =
useRef<UnregisterEndpointFunction>(() => {
// no-op
});

// @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
// @see https://github.com/GoogleChromeLabs/comlink/issues/674
// Perhaps this hook should be global and only once per whole app for now?
const registerEndpoint = useCallback<RegisterEndpointFunction>(
(endpoint) => {
unregisterPreviouslyExposedEndpointListenerRef.current();

const signer = farcasterSignerRef.current.signer;

if (signer?.status !== "approved") {
onSignerNotApprovedRef.current();
return;

return () => {
// no-op
};
}

expose(
unregisterPreviouslyExposedEndpointListenerRef.current = expose(
{
close() {
const handler = closeRef.current;
Expand Down Expand Up @@ -157,6 +174,8 @@ export function useFrameApp({
endpoint,
[new URL(frame.button.action.url).origin]
);

return unregisterPreviouslyExposedEndpointListenerRef.current;
},
[
clientRef,
Expand All @@ -179,21 +198,66 @@ type UseFrameAppInIframeReturn = {
onLoad: (event: React.SyntheticEvent<HTMLIFrameElement>) => void;
};

/**
* Handles frame app in iframe.
*
* On unmount it automatically unregisters the endpoint listener.
*
* @example
* ```
* import { useFrameAppInIframe } from '@frames.js/render/unstable-use-frame-app';
* import { useWalletClient } from 'wagmi';
* import { useFarcasterSigner } from '@frames.js/render/identity/farcaster';
*
* function MyAppDialog() {
* const walletClient = useWalletClient();
* const farcasterSigner = useFarcasterSigner({
* // ...
* });
* const frameApp = useFrameAppInIframe({
* walletClient,
* farcasterSigner,
* // frame returned by useFrame() hook
* frame: frameState.frame,
* // ... handlers for frame app actions
* });
*
* return <iframe ref={frameApp.ref} />;
* }
* ```
*/
export function useFrameAppInIframe(
options: UseFrameAppOptions
): UseFrameAppInIframeReturn {
const frameApp = useFrameApp(options);
const unregisterEndpointRef = useRef<UnregisterEndpointFunction>(() => {
// no-op
});

useEffect(() => {
return () => {
unregisterEndpointRef.current();
};
}, []);

return useMemo(() => {
return {
onLoad(event) {
if (!(event.currentTarget instanceof HTMLIFrameElement)) {
// eslint-disable-next-line no-console -- provide feedback to the developer
console.error(
'@frames.js/render/unstable-use-frame-app: "onLoad" called but event target is not an iframe'
);

return;
}
if (!event.currentTarget.contentWindow) {
return;
}

frameApp.registerEndpoint(
windowEndpoint(event.currentTarget.contentWindow)
);
const endpoint = windowEndpoint(event.currentTarget.contentWindow);

unregisterEndpointRef.current = frameApp.registerEndpoint(endpoint);
},
};
}, [frameApp]);
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3393,6 +3393,11 @@
superstruct "^1.0.3"
uuid "^9.0.1"

"@michalkvasnicak/comlink@^4.5.0":
version "4.5.0"
resolved "https://registry.yarnpkg.com/@michalkvasnicak/comlink/-/comlink-4.5.0.tgz#6fd32a5383c6f925015851523d9757498281eb06"
integrity sha512-Vi81JHsOG/y8KCpscKnwlVuSt2Vu+xo6J3d59H6geFfj1sJ/BGyg6iosQDGmzPNNZmsXH2tVEkgYlHkAtA5Y9w==

"@microsoft/tsdoc-config@0.16.2":
version "0.16.2"
resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf"
Expand Down

0 comments on commit 344fc74

Please sign in to comment.