@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/dist/index.min.js +18 -2
- package/dist/index.min.js.map +4 -4
- package/dist/rn-func-proxy.d.ts +8 -0
- package/dist/rn-func-proxy.js +67 -0
- package/dist/rn.d.ts +12 -0
- package/dist/rn.js +86 -0
- package/dist/sdk.js +11 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/rn-func-proxy.ts +110 -0
- package/src/rn.ts +124 -0
- package/src/sdk.ts +14 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farcaster/frame-sdk",
|
|
3
|
-
"version": "0.0.0-canary-
|
|
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-
|
|
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
|
-
|
|
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
|
|