Skip to content

Commit

Permalink
introduce frame-kit sdk WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
deodad committed Nov 23, 2024
1 parent ed6a9cb commit 308eba6
Show file tree
Hide file tree
Showing 26 changed files with 635 additions and 7 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"test": "./node_modules/.bin/turbo run test",
"test:ci": "./node_modules/.bin/turbo run test:ci -- --passWithNoTests",
"lint": "./node_modules/.bin/turbo run lint --parallel",
"lint:fix": "./node_modules/.bin/turbo run lint:fix --parallel",
"lint:ci": "./node_modules/.bin/turbo run lint:ci --parallel",
"prepare": "husky install",
"version-packages": "changeset version",
Expand Down
21 changes: 21 additions & 0 deletions packages/frame-core/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Logs
logs
*.log
yarn-debug.log*
yarn-error.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
23 changes: 23 additions & 0 deletions packages/frame-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@farcaster/frame-core",
"version": "0.0.1",
"main": "dist/index.js",
"scripts": {
"clean": "rimraf dist",
"prebuild": "npm run clean",
"build": "tsc",
"preversion": "npm run build",
"postversion": "git push --follow-tags"
},
"files": [
"dist",
"src"
],
"devDependencies": {
"rimraf": "^3.0.2",
"typescript": "^5.6.3"
},
"dependencies": {
"ox": "^0.1.6"
}
}
1 change: 1 addition & 0 deletions packages/frame-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./types";
40 changes: 40 additions & 0 deletions packages/frame-core/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Provider, RpcSchema } from "ox";

export type SetPrimaryButton = (options: {
text: string;
loading?: boolean;
disabled?: boolean;
hidden?: boolean;
}) => void;

export type EthProviderRequest = Provider.RequestFn<RpcSchema.Default>;

export type AccountLocation = {
placeId: string;
/**
* Human-readable string describing the location
*/
description: string;
}

export type FrameContext = {
fid: number;
custodyAddress: [];
username?: string;
displayName?: string;
/**
* Profile image URL
*/
pfpUrl?: string;
location?: AccountLocation;
}

export type FrameHost = {
context: FrameContext;
close: () => void;
ready: () => void;
openUrl: (url: string) => void;
setPrimaryButton: SetPrimaryButton;
ethProviderRequest: EthProviderRequest;
}

14 changes: 14 additions & 0 deletions packages/frame-core/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"include": ["src"],
"compilerOptions": {
"target": "es2018",
"outDir": "dist",
"lib": ["dom", "esnext"],
"declaration": true,
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
21 changes: 21 additions & 0 deletions packages/frame-host/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Logs
logs
*.log
yarn-debug.log*
yarn-error.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
29 changes: 29 additions & 0 deletions packages/frame-host/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@farcaster/frame-host",
"version": "0.0.1",
"main": "dist/index.js",
"scripts": {
"clean": "rimraf dist",
"prebuild": "npm run clean",
"build": "tsc",
"preversion": "npm run build",
"postversion": "git push --follow-tags"
},
"files": [
"dist",
"src"
],
"devDependencies": {
"react-native-webview": "^13.8.2",
"rimraf": "^3.0.2",
"typescript": "^5.6.3"
},
"peerDependencies": {
"comlink": ">=4.0.0",
"react": ">=18.0.0",
"react-native-webview": ">=13.0.0"
},
"dependencies": {
"@farcaster/frame-core": "*"
}
}
39 changes: 39 additions & 0 deletions packages/frame-host/src/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as Comlink from "comlink";
import { RefObject, useCallback, useEffect, useRef } from "react";
import WebView, { WebViewMessageEvent, WebViewProps } from "react-native-webview";
import { WebViewEndpoint, createWebViewRpcEndpoint } from "./endpoint";
import type { FrameHost } from "@farcaster/frame-core";

/**
* Returns a handler of RPC message from WebView.
*/
export function useWebViewRpcAdapter(webViewRef: RefObject<WebView>, sdk: FrameHost) {
const endpointRef = useRef<
WebViewEndpoint & {
emit: (data: any) => void;
}
>();

const onMessage: WebViewProps["onMessage"] = useCallback(
(e: WebViewMessageEvent) => {
endpointRef.current?.onMessage(e);
},
[endpointRef],
);

useEffect(() => {
const endpoint = createWebViewRpcEndpoint(webViewRef);
endpointRef.current = endpoint;

Comlink.expose(sdk, endpoint);

return () => {
endpointRef.current = undefined;
};
}, [webViewRef, sdk]);

return {
onMessage,
emit: endpointRef.current?.emit,
};
}
81 changes: 81 additions & 0 deletions packages/frame-host/src/endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import * as Comlink from "comlink";
import { RefObject } from "react";
import WebView, { WebViewMessageEvent } from "react-native-webview";

export const SYMBOL_IGNORING_RPC_RESPONSE_ERROR: symbol = Symbol();

export type WebViewEndpoint = Comlink.Endpoint & {
/**
* Manually distribute events to listeners as an alternative to `document.addEventHandler` which is unavailable in React Native.
*/
onMessage: (e: WebViewMessageEvent) => void;
};

/**
* An endpoint of communicating with WebView
*/
export function createWebViewRpcEndpoint(ref: RefObject<WebView>): WebViewEndpoint & { emit: (data: any) => void } {
const listeners: EventListenerOrEventListenerObject[] = [];
return {
addEventListener: (type, listener) => {
if (type !== "message") {
throw Error(`Got an unexpected event type "${type}". Expected "message".`);
}
listeners.push(listener);
},
removeEventListener: (type, listener) => {
if (type !== "message") {
throw Error(`Got an unexpected event type "${type}". Expected "message".`);
}
listeners.splice(listeners.findIndex((l) => l === listener));
},
postMessage: (data) => {
if (!ref.current) {
if ("value" in data && data.value === SYMBOL_IGNORING_RPC_RESPONSE_ERROR) {
return;
}
throw Error("Failed to return RPC response to WebView via postMessage");
}
console.debug("[webview:res]", data);
const dataStr = JSON.stringify(data);
return ref.current.injectJavaScript(`
console.debug('[webview:res]', ${dataStr});
document.dispatchEvent(new MessageEvent('FarcasterFrameCallback', { data: ${dataStr} }));
`);
},
onMessage: (e) => {
const data = JSON.parse(e.nativeEvent.data);
console.debug("[webview:req]", data);
const messageEvent = new MessageEvent(data);
listeners.forEach((l) => {
if (typeof l === "function") {
// Actually, messageEvent doesn't satisfy Event interface,
// but it satisfies the minimum properties that Comlink's listener requires.
l(messageEvent as unknown as Event);
} else {
l.handleEvent(messageEvent as unknown as Event);
}
});
},
emit: (data) => {
if (!ref.current) {
throw Error("Failed to send Event to WebView via postMessage");
}
console.debug("[webview:emit]", data);
const dataStr = JSON.stringify(data);
return ref.current.injectJavaScript(`
console.debug('[webview:emit]', ${dataStr});
document.dispatchEvent(new MessageEvent('FarcasterFrameEvent', { data: ${dataStr} }));
`);
},
};
}

/**
* Standard MessageEvent is unavailable in React Native since it's part of HTML Standard.
* Instead, implement our own MessageEvent with the minimum properties required by Comlink implementation.
*/
class MessageEvent {
public origin = "ReactNativeWebView";
constructor(public data: unknown) {}
}
2 changes: 2 additions & 0 deletions packages/frame-host/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./adapter";
export * from "./endpoint";
14 changes: 14 additions & 0 deletions packages/frame-host/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"include": ["src"],
"compilerOptions": {
"target": "es2018",
"outDir": "dist",
"lib": ["dom", "esnext"],
"declaration": true,
"moduleResolution": "node",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
7 changes: 7 additions & 0 deletions packages/frame-sdk/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
};
21 changes: 21 additions & 0 deletions packages/frame-sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Logs
logs
*.log
yarn-debug.log*
yarn-error.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
1 change: 1 addition & 0 deletions packages/frame-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @farcaster/frame-kit
1 change: 1 addition & 0 deletions packages/frame-sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# `@farcaster/frame-kit`
29 changes: 29 additions & 0 deletions packages/frame-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@farcaster/frame-sdk",
"version": "0.0.1",
"main": "dist/index.js",
"scripts": {
"clean": "rimraf dist",
"prebuild": "npm run clean",
"build": "tsc",
"typecheck": "tsc --noEmit",
"preversion": "npm run build",
"postversion": "git push --follow-tags"
},
"files": [
"dist",
"src"
],
"devDependencies": {
"rimraf": "^6.0.1",
"typescript": "^5.6.3"
},
"dependencies": {
"@farcaster/frame-core": "*",
"comlink": "^4.4.2",
"eventemitter3": "^5.0.1"
},
"peerDependencies": {
"ox": "^0.2.2"
}
}
19 changes: 19 additions & 0 deletions packages/frame-sdk/src/endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type Endpoint, windowEndpoint } from "comlink";

const webViewEndpoint: Endpoint = {
postMessage: (data: unknown) => {
console.debug("[webview:req]", data);
window.ReactNativeWebView.postMessage(JSON.stringify(data));
},
addEventListener: (_, listener, ...args) => {
document.addEventListener("FarcasterFrameCallback", listener, ...args);
},
removeEventListener: (_, listener) => {
document.removeEventListener("FarcasterFrameCallback", listener);
},
};

export const endpoint = window?.ReactNativeWebView
? webViewEndpoint
: // TODO fallback cleanly when not in iFrame or webview
windowEndpoint(window?.parent ?? window);
5 changes: 5 additions & 0 deletions packages/frame-sdk/src/frameHost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { wrap } from "comlink";
import { endpoint } from "./endpoint";
import { FrameHost } from "@farcaster/frame-core";

export const frameHost = wrap<FrameHost>(endpoint);
2 changes: 2 additions & 0 deletions packages/frame-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./sdk";
export * from "./provider";
Loading

0 comments on commit 308eba6

Please sign in to comment.