1
1
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" ;
4
8
import type { FrameHost , SetPrimaryButton } from "@farcaster/frame-sdk" ;
5
9
import type { UseWalletClientReturnType } from "wagmi" ;
6
10
import { useFreshRef } from "./hooks/use-fresh-ref" ;
@@ -53,7 +57,11 @@ type UseFrameAppOptions = {
53
57
onPrimaryButtonSet ?: SetPrimaryButton ;
54
58
} ;
55
59
56
- type RegisterEndpointFunction = ( endpoint : Endpoint ) => void ;
60
+ type UnregisterEndpointFunction = ( ) => void ;
61
+
62
+ type RegisterEndpointFunction = (
63
+ endpoint : Endpoint
64
+ ) => UnregisterEndpointFunction ;
57
65
58
66
type UseFrameAppReturn = {
59
67
/**
@@ -82,20 +90,29 @@ export function useFrameApp({
82
90
const onSignerNotApprovedRef = useFreshRef ( onSignerNotApproved ) ;
83
91
const onPrimaryButtonSetRef = useFreshRef ( onPrimaryButtonSet ) ;
84
92
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
+ } ) ;
85
100
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?
89
101
const registerEndpoint = useCallback < RegisterEndpointFunction > (
90
102
( endpoint ) => {
103
+ unregisterPreviouslyExposedEndpointListenerRef . current ( ) ;
104
+
91
105
const signer = farcasterSignerRef . current . signer ;
92
106
93
107
if ( signer ?. status !== "approved" ) {
94
108
onSignerNotApprovedRef . current ( ) ;
95
- return ;
109
+
110
+ return ( ) => {
111
+ // no-op
112
+ } ;
96
113
}
97
114
98
- expose (
115
+ unregisterPreviouslyExposedEndpointListenerRef . current = expose (
99
116
{
100
117
close ( ) {
101
118
const handler = closeRef . current ;
@@ -157,6 +174,8 @@ export function useFrameApp({
157
174
endpoint ,
158
175
[ new URL ( frame . button . action . url ) . origin ]
159
176
) ;
177
+
178
+ return unregisterPreviouslyExposedEndpointListenerRef . current ;
160
179
} ,
161
180
[
162
181
clientRef ,
@@ -179,21 +198,66 @@ type UseFrameAppInIframeReturn = {
179
198
onLoad : ( event : React . SyntheticEvent < HTMLIFrameElement > ) => void ;
180
199
} ;
181
200
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
+ */
182
229
export function useFrameAppInIframe (
183
230
options : UseFrameAppOptions
184
231
) : UseFrameAppInIframeReturn {
185
232
const frameApp = useFrameApp ( options ) ;
233
+ const unregisterEndpointRef = useRef < UnregisterEndpointFunction > ( ( ) => {
234
+ // no-op
235
+ } ) ;
236
+
237
+ useEffect ( ( ) => {
238
+ return ( ) => {
239
+ unregisterEndpointRef . current ( ) ;
240
+ } ;
241
+ } , [ ] ) ;
186
242
187
243
return useMemo ( ( ) => {
188
244
return {
189
245
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
+ }
190
254
if ( ! event . currentTarget . contentWindow ) {
191
255
return ;
192
256
}
193
257
194
- frameApp . registerEndpoint (
195
- windowEndpoint ( event . currentTarget . contentWindow )
196
- ) ;
258
+ const endpoint = windowEndpoint ( event . currentTarget . contentWindow ) ;
259
+
260
+ unregisterEndpointRef . current = frameApp . registerEndpoint ( endpoint ) ;
197
261
} ,
198
262
} ;
199
263
} , [ frameApp ] ) ;
0 commit comments