@farcaster/frame-sdk 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/frame-sdk",
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",
@@ -22,7 +22,8 @@
22
22
  "comlink": "^4.4.2",
23
23
  "eventemitter3": "^5.0.1",
24
24
  "ox": "^0.4.4",
25
- "@farcaster/frame-core": "0.0.0-canary-20250508220243"
25
+ "@farcaster/frame-core": "0.0.0-canary-20250508220359",
26
+ "@farcaster/frame-host": "0.0.0-canary-20250508220359"
26
27
  },
27
28
  "scripts": {
28
29
  "clean": "rm -rf dist",
@@ -0,0 +1,110 @@
1
+ import {
2
+ type TransferHandler,
3
+ type WireValue,
4
+ fromWireValue,
5
+ proxyMarker,
6
+ toWireValue,
7
+ } from '@farcaster/frame-host'
8
+ import { isInReactNativeWebViewEnvironment } from './rn'
9
+
10
+ // Since Comlink's proxyTransferHandler doesn't work on React Native, we need to
11
+ // manually serialize and deserialize functions.
12
+ // We associate each function with a unique ID and store it in a map so the host
13
+ // receives a function stub that when called passes the ID and arguments to the
14
+ // WebView which then looks up the function by ID and calls it.
15
+
16
+ export const proxiedFunctionsForReactNative = new Map<string, Function>()
17
+ let nextFunctionId = 0
18
+
19
+ function generateFunctionId(): string {
20
+ return `rn_webview_fn_proxy_${nextFunctionId++}`
21
+ }
22
+
23
+ const comlinkThrownValueMarker = Symbol.for('Comlink.thrown')
24
+
25
+ export const reactNativeFunctionSerializerTransferHandler: TransferHandler<
26
+ Function,
27
+ { __isRNProxiedFunction: true; id: string; type: 'function' }
28
+ > = {
29
+ canHandle: (value: unknown): value is Function => {
30
+ if (isInReactNativeWebViewEnvironment() && typeof value === 'function') {
31
+ // In RN WebView, we force this handler for all functions to ensure ID-based proxying,
32
+ // as standard MessageChannel-based proxying won't work.
33
+ // This overrides Comlink's default proxyTransferHandler if the function was pre-marked.
34
+ return true
35
+ }
36
+ return false
37
+ },
38
+ serialize: (funcToProxy: Function) => {
39
+ const functionId = generateFunctionId()
40
+ proxiedFunctionsForReactNative.set(functionId, funcToProxy)
41
+ console.debug(
42
+ `[WebView RN Comlink] Serializing function (name: '${
43
+ funcToProxy.name || 'anonymous'
44
+ }', hasProxyMarker: ${!!(funcToProxy as any)[proxyMarker]}) to ID: ${functionId}`,
45
+ )
46
+ return [
47
+ { __isRNProxiedFunction: true, id: functionId, type: 'function' },
48
+ [],
49
+ ]
50
+ },
51
+ deserialize: (serializedValue: {
52
+ __isRNProxiedFunction: true
53
+ id: string
54
+ type: 'function'
55
+ }): Function => {
56
+ const func = proxiedFunctionsForReactNative.get(serializedValue.id)
57
+ if (!func) {
58
+ console.error(
59
+ `[WebView RN Comlink] Proxied function with ID ${serializedValue.id} not found during deserialization on WebView.`,
60
+ )
61
+ throw new Error(
62
+ `Proxied function with ID ${serializedValue.id} not found on WebView.`,
63
+ )
64
+ }
65
+ console.debug(
66
+ `[WebView RN Comlink] Deserializing ID ${serializedValue.id} back to function on WebView.`,
67
+ )
68
+ return func
69
+ },
70
+ }
71
+
72
+ export async function _callWebViewProxiedFunction(
73
+ functionId: string,
74
+ wireArgs: WireValue[],
75
+ ): Promise<WireValue> {
76
+ const func = proxiedFunctionsForReactNative.get(functionId)
77
+
78
+ if (!func) {
79
+ console.error(
80
+ `[WebView RN Comlink] Proxied function with ID ${functionId} not found for execution.`,
81
+ )
82
+ const error = new Error(
83
+ `Proxied function with ID ${functionId} not found on WebView.`,
84
+ )
85
+ const valueWithMarker = { value: error, [comlinkThrownValueMarker]: 0 }
86
+ const [wireError] = toWireValue(valueWithMarker)
87
+ return wireError
88
+ }
89
+
90
+ const args = wireArgs.map((arg) => fromWireValue(arg))
91
+
92
+ try {
93
+ const result = await func(...args)
94
+ const [wireResult, transferables] = toWireValue(result)
95
+ if (transferables.length > 0) {
96
+ console.warn(
97
+ '[WebView RN Comlink] Transferables returned from _callWebViewProxiedFunction are ignored.',
98
+ )
99
+ }
100
+ return wireResult
101
+ } catch (error) {
102
+ console.error(
103
+ `[WebView RN Comlink] Error executing proxied function ID ${functionId}:`,
104
+ error,
105
+ )
106
+ const valueWithMarker = { value: error, [comlinkThrownValueMarker]: 0 }
107
+ const [wireError] = toWireValue(valueWithMarker)
108
+ return wireError
109
+ }
110
+ }
package/src/rn.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { expose, transferHandlers } from 'comlink'
2
+ import {
3
+ _callWebViewProxiedFunction,
4
+ proxiedFunctionsForReactNative,
5
+ reactNativeFunctionSerializerTransferHandler,
6
+ } from './rn-func-proxy'
7
+
8
+ const comlinkListeners = new Map<EventListener, EventListener>()
9
+
10
+ /**
11
+ * @returns true if the environment is a React Native WebView, false otherwise.
12
+ */
13
+ export function isInReactNativeWebViewEnvironment(): boolean {
14
+ return typeof window !== 'undefined' && !!(window as any).ReactNativeWebView
15
+ }
16
+
17
+ /**
18
+ * Initializes the React Native SDK.
19
+ * This function should be called before the SDK is used.
20
+ *
21
+ * This is necessary because Complink function proxing doesn't work on React Native
22
+ * so we have our custom implementation we need to expose to the host.
23
+ */
24
+ export const initializeReactNativeSDK = () => {
25
+ if (isInReactNativeWebViewEnvironment()) {
26
+ if (!transferHandlers.has('rn_function')) {
27
+ transferHandlers.set(
28
+ 'rn_function',
29
+ reactNativeFunctionSerializerTransferHandler as any,
30
+ )
31
+ console.debug(
32
+ '[WebView RN Comlink] Registered reactNativeFunctionSerializerTransferHandler.',
33
+ )
34
+ }
35
+
36
+ const sdkApiToExpose = {
37
+ _callWebViewProxiedFunction,
38
+ }
39
+
40
+ if (window.ReactNativeWebView) {
41
+ const endpointForHostViaRN = {
42
+ postMessage: (message: any, _transfer?: Transferable[]) => {
43
+ if (window.ReactNativeWebView) {
44
+ console.debug('[SDK->Host Comlink] postMessage', message)
45
+ window.ReactNativeWebView.postMessage(JSON.stringify(message))
46
+ } else {
47
+ console.error(
48
+ '[SDK->Host Comlink] ReactNativeWebView became undefined.',
49
+ )
50
+ }
51
+ },
52
+ addEventListener: (
53
+ type: string,
54
+ listener: EventListenerOrEventListenerObject,
55
+ ) => {
56
+ if (type === 'message' && typeof listener === 'function') {
57
+ const adaptedListener = (event: Event) => {
58
+ const me = event as MessageEvent
59
+ const comlinkMessageData = me.data
60
+
61
+ if (!comlinkMessageData) {
62
+ console.warn(
63
+ '[SDK Comlink Endpoint] Received event with no data from host',
64
+ )
65
+ return
66
+ }
67
+ // Comlink's listener expects a MessageEvent-like object with a `data` property.
68
+ // Cast to `any` to satisfy the EventListenerOrEventListenerObject type constraint,
69
+ // while providing the structure Comlink needs.
70
+ listener({ data: comlinkMessageData } as any)
71
+ }
72
+ comlinkListeners.set(listener, adaptedListener)
73
+ document.addEventListener('FarcasterFrameCallback', adaptedListener)
74
+ }
75
+ },
76
+ removeEventListener: (
77
+ type: string,
78
+ listener: EventListenerOrEventListenerObject,
79
+ ) => {
80
+ if (type === 'message' && typeof listener === 'function') {
81
+ const adaptedListener = comlinkListeners.get(listener)
82
+ if (adaptedListener) {
83
+ console.debug('[SDK->Host Comlink] removeEventListener', listener)
84
+ document.removeEventListener(
85
+ 'FarcasterFrameCallback',
86
+ adaptedListener,
87
+ )
88
+ comlinkListeners.delete(listener)
89
+ }
90
+ }
91
+ },
92
+ }
93
+
94
+ try {
95
+ expose(sdkApiToExpose, endpointForHostViaRN)
96
+ console.debug(
97
+ '[WebView RN Comlink] Exposed SDK API for RN Host:',
98
+ Object.keys(sdkApiToExpose),
99
+ )
100
+ } catch (e) {
101
+ // Comlink throws if an endpoint is already exposed.
102
+ console.warn(
103
+ '[WebView RN Comlink] Error exposing SDK API (might be already exposed): ',
104
+ e,
105
+ )
106
+ }
107
+
108
+ window.addEventListener('unload', () => {
109
+ proxiedFunctionsForReactNative.clear()
110
+ comlinkListeners.forEach((adaptedListener, _originalListener) => {
111
+ document.removeEventListener(
112
+ 'FarcasterFrameCallback',
113
+ adaptedListener,
114
+ )
115
+ })
116
+ comlinkListeners.clear()
117
+ })
118
+ } else {
119
+ console.warn(
120
+ '[WebView RN Comlink] ReactNativeWebView not found, cannot expose SDK API to host.',
121
+ )
122
+ }
123
+ }
124
+ }
package/src/sdk.ts CHANGED
@@ -8,8 +8,17 @@ import { proxy } from 'comlink'
8
8
  import { EventEmitter } from 'eventemitter3'
9
9
  import { frameHost } from './frameHost'
10
10
  import { provider } from './provider'
11
+ import {
12
+ initializeReactNativeSDK,
13
+ isInReactNativeWebViewEnvironment,
14
+ } from './rn'
11
15
  import type { Emitter, EventMap, FrameSDK } from './types'
12
16
 
17
+ // We need to ensure the React Native specific parts of the SDK are initialized
18
+ // before the SDK is used. Otherwise certain functionality, such as proxied function
19
+ // calls, will not work.
20
+ initializeReactNativeSDK()
21
+
13
22
  export function createEmitter(): Emitter {
14
23
  const emitter = new EventEmitter<EventMap>()
15
24
 
@@ -131,7 +140,11 @@ export const sdk: FrameSDK = {
131
140
  ethProvider: provider,
132
141
  },
133
142
  setShareStateProvider: (fn: ShareStateProvider) => {
134
- frameHost.setShareStateProvider.bind(frameHost)(proxy(fn))
143
+ if (isInReactNativeWebViewEnvironment()) {
144
+ frameHost.setShareStateProvider.bind(frameHost)(fn as any)
145
+ } else {
146
+ frameHost.setShareStateProvider.bind(frameHost)(proxy(fn))
147
+ }
135
148
  },
136
149
  }
137
150