@farcaster/frame-host-react-native 0.0.0-canary-20250508220243 → 0.0.0-canary-20250508220359

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
- import { exposeToEndpoint, useExposeToEndpoint } from '@farcaster/frame-host';
1
+ import { exposeToEndpoint } from '@farcaster/frame-host';
2
2
  import { useCallback, useEffect, useMemo, useState, } from 'react';
3
+ import { releaseProxy, wrapWebViewForRnComlink } from './rn-comlink';
3
4
  import { createWebViewRpcEndpoint } from './webview';
4
5
  /**
5
6
  * Returns a handler of RPC message from WebView.
@@ -12,6 +13,7 @@ export function useWebViewRpcAdapter({ webViewRef, domain, sdk, ethProvider, deb
12
13
  useEffect(() => {
13
14
  const newEndpoint = createWebViewRpcEndpoint(webViewRef, domain);
14
15
  setEndpoint(newEndpoint);
16
+ const webViewProxy = wrapWebViewForRnComlink(newEndpoint);
15
17
  const cleanup = exposeToEndpoint({
16
18
  endpoint: newEndpoint,
17
19
  sdk,
@@ -21,6 +23,7 @@ export function useWebViewRpcAdapter({ webViewRef, domain, sdk, ethProvider, deb
21
23
  });
22
24
  return () => {
23
25
  cleanup?.();
26
+ webViewProxy?.[releaseProxy]?.();
24
27
  setEndpoint(undefined);
25
28
  };
26
29
  }, [webViewRef, domain, sdk, ethProvider, debug]);
@@ -48,11 +51,22 @@ export function useWebViewRpcEndpoint(webViewRef, domain) {
48
51
  }), [endpoint, onMessage]);
49
52
  }
50
53
  export function useExposeWebViewToEndpoint({ endpoint, sdk, ethProvider, debug = false, }) {
51
- useExposeToEndpoint({
52
- endpoint,
53
- sdk,
54
- frameOrigin: 'ReactNativeWebView',
55
- ethProvider,
56
- debug,
57
- });
54
+ useEffect(() => {
55
+ let webViewProxy = null;
56
+ let cleanup = null;
57
+ if (endpoint) {
58
+ webViewProxy = wrapWebViewForRnComlink(endpoint);
59
+ cleanup = exposeToEndpoint({
60
+ endpoint,
61
+ sdk,
62
+ ethProvider,
63
+ frameOrigin: 'ReactNativeWebView',
64
+ debug,
65
+ });
66
+ }
67
+ return () => {
68
+ cleanup?.();
69
+ webViewProxy?.[releaseProxy]?.();
70
+ };
71
+ }, [endpoint, sdk, ethProvider, debug]);
58
72
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/frame-host-react-native",
3
- "version": "0.0.0-canary-20250508220243",
3
+ "version": "0.0.0-canary-20250508220359",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "ox": "^0.4.4",
29
- "@farcaster/frame-host": "0.0.0-canary-20250508220243"
29
+ "@farcaster/frame-host": "0.0.0-canary-20250508220359"
30
30
  },
31
31
  "scripts": {
32
32
  "clean": "rm -rf dist",
@@ -0,0 +1,108 @@
1
+ import {
2
+ type Remote,
3
+ type TransferHandler,
4
+ type WireValue,
5
+ WireValueType,
6
+ fromWireValue,
7
+ toWireValue,
8
+ } from '@farcaster/frame-host'
9
+
10
+ // The shape of the serialized function reference coming from the WebView
11
+ interface SerializedRnFunction {
12
+ __isRNProxiedFunction: true
13
+ id: string
14
+ type: 'function'
15
+ }
16
+
17
+ function processArgumentsForRN(
18
+ argumentList: any[],
19
+ ): [WireValue[], Transferable[]] {
20
+ const processed = argumentList.map((arg) => toWireValue(arg))
21
+ const wireValues = processed.map((p) => p[0])
22
+ const transferables = processed.flatMap((p) => p[1])
23
+ if (transferables.length > 0) {
24
+ console.warn(
25
+ '[RN Host Comlink] Transferables found in arguments to WebView proxied function are ignored.',
26
+ )
27
+ }
28
+ return [wireValues, []]
29
+ }
30
+
31
+ export interface ReactNativeWebViewProxy {
32
+ _callWebViewProxiedFunction: (
33
+ id: string,
34
+ args: WireValue[],
35
+ ) => Promise<WireValue>
36
+ }
37
+
38
+ /**
39
+ * Creates a Comlink TransferHandler for deserializing functions proxied from WebView
40
+ * and serializing function stubs created by this handler.
41
+ */
42
+ export function createReactNativeFunctionDeserializer(
43
+ getWebViewProxy: () => Remote<ReactNativeWebViewProxy> | undefined,
44
+ ): TransferHandler<Function, SerializedRnFunction> {
45
+ return {
46
+ // canHandle checks if a given value is a Function stub that this handler can serialize.
47
+ canHandle: (value: unknown): value is Function => {
48
+ return (
49
+ typeof value === 'function' &&
50
+ Object.hasOwn(value, '__functionId__') &&
51
+ typeof (value as any).__functionId__ === 'string'
52
+ )
53
+ },
54
+ // deserialize converts the wire format (SerializedRnFunction) into a callable Function stub.
55
+ deserialize: (serializedValue: SerializedRnFunction): Function => {
56
+ const functionId = serializedValue.id
57
+ console.debug(
58
+ `[RN Host Comlink] Creating stub for WebView function ID: ${functionId}`,
59
+ )
60
+ const webViewFunctionStub = async (...args: any[]) => {
61
+ const webViewProxy = getWebViewProxy()
62
+ if (!webViewProxy || !webViewProxy._callWebViewProxiedFunction) {
63
+ console.error(
64
+ '[RN Host Comlink] WebView proxy or _callWebViewProxiedFunction not available.',
65
+ )
66
+ throw new Error(
67
+ '[RN Host Comlink] WebView proxy not available. Cannot call remote function.',
68
+ )
69
+ }
70
+ console.debug(
71
+ `[RN Host Comlink] Calling WebView function ID: ${functionId} with args:`,
72
+ args,
73
+ )
74
+ const [wireArgs] = processArgumentsForRN(args)
75
+ try {
76
+ const typedProxy = webViewProxy as Remote<ReactNativeWebViewProxy>
77
+ const wireResult = await typedProxy._callWebViewProxiedFunction(
78
+ functionId,
79
+ wireArgs,
80
+ )
81
+ if (
82
+ wireResult &&
83
+ wireResult.type === WireValueType.HANDLER &&
84
+ (wireResult as any).name === 'throw'
85
+ ) {
86
+ return fromWireValue(wireResult)
87
+ }
88
+ return fromWireValue(wireResult)
89
+ } catch (error) {
90
+ console.error(
91
+ `[RN Host Comlink] Error in stub for function ID ${functionId}:`,
92
+ error,
93
+ )
94
+ throw error
95
+ }
96
+ }
97
+ ;(webViewFunctionStub as any).__functionId__ = functionId
98
+ return webViewFunctionStub
99
+ },
100
+ // serialize converts a Function stub (that this handler knows about) into its wire format.
101
+ serialize: (fnStub: Function): [SerializedRnFunction, Transferable[]] => {
102
+ // canHandle should have already verified this is one of our stubs.
103
+ const id = (fnStub as any).__functionId__
104
+ console.debug(`[RN Host Comlink] Serializing function stub ID: ${id}`)
105
+ return [{ __isRNProxiedFunction: true, id, type: 'function' }, []]
106
+ },
107
+ }
108
+ }
@@ -0,0 +1,130 @@
1
+ import {
2
+ type FrameHost,
3
+ type Endpoint as FrameHostEndpoint,
4
+ type Remote as FrameHostRemote,
5
+ type TransferHandler as FrameHostTransferHandler,
6
+ expose,
7
+ releaseProxy,
8
+ transferHandlers,
9
+ wrap,
10
+ } from '@farcaster/frame-host'
11
+ import {
12
+ forwardProviderEvents,
13
+ wrapProviderRequest,
14
+ } from '@farcaster/frame-host/src/helpers/provider'
15
+ import { wrapHandlers } from '@farcaster/frame-host/src/helpers/sdk'
16
+ import type { Provider } from 'ox/Provider'
17
+ import {
18
+ type ReactNativeWebViewProxy,
19
+ createReactNativeFunctionDeserializer,
20
+ } from './rn-comlink-helpers'
21
+
22
+ // Re-export releaseProxy symbol for use in cleanup
23
+ export { releaseProxy }
24
+
25
+ let rnHandlersInitialized = false
26
+ let webViewProxyInstance: FrameHostRemote<ReactNativeWebViewProxy> | null = null
27
+ let rnFunctionDeserializer: FrameHostTransferHandler<any, any> | null = null
28
+
29
+ /**
30
+ * Ensures React Native specific Comlink transfer handlers are registered globally.
31
+ */
32
+ export function ensureRnComlinkHandlers(): void {
33
+ if (rnHandlersInitialized) {
34
+ return
35
+ }
36
+
37
+ const getWebViewProxy = ():
38
+ | FrameHostRemote<ReactNativeWebViewProxy>
39
+ | undefined => {
40
+ return webViewProxyInstance === null ? undefined : webViewProxyInstance
41
+ }
42
+
43
+ rnFunctionDeserializer =
44
+ createReactNativeFunctionDeserializer(getWebViewProxy)
45
+ transferHandlers.set(
46
+ 'rn_function',
47
+ rnFunctionDeserializer as FrameHostTransferHandler<any, any>,
48
+ )
49
+ console.debug(
50
+ '[RN Host Comlink] Registered ReactNativeFunctionDeserializer via ensureRnComlinkHandlers.',
51
+ )
52
+ rnHandlersInitialized = true
53
+ }
54
+
55
+ /**
56
+ * Wraps a WebView endpoint with Comlink, ensuring RN handlers are set up.
57
+ * Crucially, it sets the webViewProxyInstance needed by the deserializer's callback.
58
+ *
59
+ * If this is not called, deserializing the function will fail.
60
+ */
61
+ export function wrapWebViewForRnComlink<T extends ReactNativeWebViewProxy>(
62
+ endpoint: FrameHostEndpoint,
63
+ ): FrameHostRemote<T> {
64
+ ensureRnComlinkHandlers()
65
+
66
+ const proxy: FrameHostRemote<T> = wrap<T>(
67
+ endpoint,
68
+ ) as unknown as FrameHostRemote<T>
69
+
70
+ webViewProxyInstance = proxy as FrameHostRemote<ReactNativeWebViewProxy>
71
+
72
+ return proxy
73
+ }
74
+
75
+ /**
76
+ * Exposes the Host SDK object to the WebView, ensuring RN handlers are set up
77
+ * on the Comlink instance used for the exposure.
78
+ * Replicates logic from `@farcaster/frame-host`'s `exposeToEndpoint` helper
79
+ * but makes sure to set up the RN handlers first.
80
+ *
81
+ * @returns Cleanup function if any event listeners were added (e.g., provider events)
82
+ */
83
+ export function exposeToWebViewWithRnComlink({
84
+ endpoint,
85
+ sdk,
86
+ ethProvider,
87
+ debug = false,
88
+ }: {
89
+ endpoint: FrameHostEndpoint
90
+ sdk: Omit<FrameHost, 'ethProviderRequestV2'>
91
+ ethProvider?: Provider
92
+ debug?: boolean
93
+ }): (() => void) | undefined {
94
+ ensureRnComlinkHandlers()
95
+
96
+ const extendedSdk = wrapHandlers(sdk as FrameHost)
97
+
98
+ let providerCleanup: (() => void) | undefined
99
+ if (ethProvider) {
100
+ extendedSdk.ethProviderRequestV2 = wrapProviderRequest({
101
+ provider: ethProvider,
102
+ debug,
103
+ })
104
+ try {
105
+ providerCleanup = forwardProviderEvents({
106
+ provider: ethProvider,
107
+ endpoint: endpoint as any,
108
+ })
109
+ } catch (e) {
110
+ console.error(
111
+ '[RN Host Comlink] Error setting up provider event forwarding:',
112
+ e,
113
+ )
114
+ }
115
+ }
116
+
117
+ try {
118
+ expose(extendedSdk, endpoint)
119
+ console.debug(
120
+ '[RN Host Comlink] Exposed SDK to WebView via exposeToWebViewWithRnComlink',
121
+ )
122
+ } catch (e) {
123
+ console.warn(
124
+ '[RN Host Comlink] Error exposing API (may already be exposed):',
125
+ e,
126
+ )
127
+ }
128
+
129
+ return providerCleanup
130
+ }
@@ -1,5 +1,8 @@
1
- import type { FrameHost } from '@farcaster/frame-host'
2
- import { exposeToEndpoint, useExposeToEndpoint } from '@farcaster/frame-host'
1
+ import type {
2
+ FrameHost,
3
+ Remote as FrameHostRemote,
4
+ } from '@farcaster/frame-host'
5
+ import { exposeToEndpoint } from '@farcaster/frame-host'
3
6
  import type { Provider } from 'ox/Provider'
4
7
  import {
5
8
  type RefObject,
@@ -10,6 +13,8 @@ import {
10
13
  } from 'react'
11
14
  import type WebView from 'react-native-webview'
12
15
  import type { WebViewMessageEvent, WebViewProps } from 'react-native-webview'
16
+ import { releaseProxy, wrapWebViewForRnComlink } from './rn-comlink'
17
+ import type { ReactNativeWebViewProxy } from './rn-comlink-helpers'
13
18
  import { type WebViewEndpoint, createWebViewRpcEndpoint } from './webview'
14
19
 
15
20
  /**
@@ -41,6 +46,9 @@ export function useWebViewRpcAdapter({
41
46
  const newEndpoint = createWebViewRpcEndpoint(webViewRef, domain)
42
47
  setEndpoint(newEndpoint)
43
48
 
49
+ const webViewProxy =
50
+ wrapWebViewForRnComlink<ReactNativeWebViewProxy>(newEndpoint)
51
+
44
52
  const cleanup = exposeToEndpoint({
45
53
  endpoint: newEndpoint,
46
54
  sdk,
@@ -51,6 +59,7 @@ export function useWebViewRpcAdapter({
51
59
 
52
60
  return () => {
53
61
  cleanup?.()
62
+ webViewProxy?.[releaseProxy]?.()
54
63
  setEndpoint(undefined)
55
64
  }
56
65
  }, [webViewRef, domain, sdk, ethProvider, debug])
@@ -107,11 +116,24 @@ export function useExposeWebViewToEndpoint({
107
116
  ethProvider?: Provider
108
117
  debug?: boolean
109
118
  }) {
110
- useExposeToEndpoint({
111
- endpoint,
112
- sdk,
113
- frameOrigin: 'ReactNativeWebView',
114
- ethProvider,
115
- debug,
116
- })
119
+ useEffect(() => {
120
+ let webViewProxy: FrameHostRemote<ReactNativeWebViewProxy> | null = null
121
+ let cleanup: (() => void) | undefined | null = null
122
+
123
+ if (endpoint) {
124
+ webViewProxy = wrapWebViewForRnComlink<ReactNativeWebViewProxy>(endpoint)
125
+
126
+ cleanup = exposeToEndpoint({
127
+ endpoint,
128
+ sdk,
129
+ ethProvider,
130
+ frameOrigin: 'ReactNativeWebView',
131
+ debug,
132
+ })
133
+ }
134
+ return () => {
135
+ cleanup?.()
136
+ webViewProxy?.[releaseProxy]?.()
137
+ }
138
+ }, [endpoint, sdk, ethProvider, debug])
117
139
  }