Skip to content

Commit

Permalink
React sdk (#212)
Browse files Browse the repository at this point in the history
* Modify calling signTypedData by wallet client

* Upload pnpm-lock.yaml

* Generator resource files

* Generate index file

* Add some config

* Add comments for method

* Fix react bundle issue

* Update README.md

* Update bundle config

* Update core-sdk version in order to solve linking react-sdk

* reset core-sdk version

* Link core sdk

* Modify core-sdk reference

* Cancel fix code command

* Set link-workspace-packages false

* Modify pnpm-lock file

* test

* Rename StoryProtocolContext

* Add comments about npmrc

* Modify package.json

* Add getPermissionSignature and export the related types

* Update README.md
  • Loading branch information
bonnie57 authored Jun 21, 2024
1 parent 289661e commit 003e6df
Show file tree
Hide file tree
Showing 18 changed files with 2,121 additions and 127 deletions.
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Once core-react is published to npm, react-sdk can be updated to use it
link-workspace-packages: false
7 changes: 7 additions & 0 deletions packages/react-sdk/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
},
extends: ["@story-protocol/eslint-config"],
};
84 changes: 84 additions & 0 deletions packages/react-sdk/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Story Protocol React SDK

The react-sdk is a library that provides a set of hooks to interact with the SDK. It is designed to be used in a React application.

## How to use Story Protocol SDK in Your Project

### Generate React SDK

1. Install the dependencies

```bash
pnpm install
```

2. Update the `@story-protocol/core-sdk` package version in the `packages/react-sdk/package.json` file to the latest version.

Important: Once publish core-sdk, you need to update the core-sdk version in the react-sdk package.json file.

3. Generate the SDK

```bash
pnpm run generate
```

This SDK is generated using the command `pnpm run generate`. The source code resides in the `packages/sdk` directory and the generated SDK can be found in the `packages/react-sdk` folder.

### How to use Story Protocol React SDK in Your Project

- Install Story Protocol React SDK

```bash
pnpm install @story-protocol/react-sdk
```

- Import the provider in your React application

```typescript
import { StoryProvider } from "@story-protocol/react-sdk";
const client = StoryClient.newClient(config);
<StoryProvider
config={{
chainId: "sepolia",
transport: http("RPC_URL"),
wallet: walletClient,
}}
</StoryProvider>
```

- Use the hooks in your component

```typescript
import { useIpAsset } from "@story-protocol/react-sdk";
const { data, error, loading, register } = useIpAsset();
register({ nftContract: "0x1234", tokenId: "1" });
```

### How To Build and Test Story Protocol React SDK for local testing

- Install yalc

```bash
npm install -g yalc
```

- For manual testing of the react-sdk, set up a separate web project. The guide below uses `yalc` to link the `react-sdk` locally, enabling its installation and import for testing.

Under the `typescript-sdk/packages/react-sdk` directory:

- Execute `npm run build` to build your latest code.
- Run `yalc publish`. You should see a message like `@story-protocol/react-sdk@<version> published in store.` (Note: The version number may vary).
- To set up your testing environment (e.g., a new Next.js project), use `yalc add @story-protocol/react-sdk@<version>` (ensure the version number is updated accordingly).

- Run `pnpm install`. This installs `@story-protocol/react-sdk@<version>` with your local changes.

### Steps to Refresh the Changes

Under the `typescript-sdk/packages/react-sdk` directory:

- Execute `npm run build` to build your latest code.
- Run `yalc push`.

In your testing environment:

- Run `yalc update` to pull the latest changes.
125 changes: 125 additions & 0 deletions packages/react-sdk/generator/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const ejs = require("ejs");
const fs = require("fs");
const path = require("path");
const ts = require("typescript");
const resourcesFolder = path.resolve(__dirname, "../../core-sdk/src/resources");
const resourceTemplate = require("./templates/resource");
const indexTemplate = require("./templates/index");

console.log("🚀🚀 React SDK generator started!\n");
const isPrimitiveType = (type) => {
return [
"string",
"number",
"boolean",
"symbol",
"undefined",
"null",
"bigint",
"string|bigint|number",
].includes(type);
};
const isViemType = (type) => {
return ["Hex", "Address"].includes(type);
};
const visit = (file) => {
let program = ts.createProgram([file], { allowJs: true });
const sourceFile = program.getSourceFile(file);
ts.createProgram([sourceFile.fileName], {}).getTypeChecker();
const publicMethods = [];
ts.forEachChild(sourceFile, (node) => {
if (ts.isClassDeclaration(node)) {
for (const member of node.members) {
if (
ts.isMethodDeclaration(member) &&
(member.modifiers?.some(
(m) => m.kind === ts.SyntaxKind.PublicKeyword
) ??
true) &&
member.name &&
ts.isIdentifier(member.name)
) {
const requests = [];
program.getTypeChecker().getSignatureFromDeclaration(member);
member.parameters.forEach((parameter) => {
requests.push({
name: parameter.name.escapedText,
type: parameter.type.getText(),
});
});
const method = {
name: member.name.text,
requests,
responseType: member.type
?.getText()
.replace("Promise<", "")
.replace(">", ""),
comments:
ts
.getLeadingCommentRanges(sourceFile.text, member.pos)
?.map((range) =>
sourceFile.text.substring(range.pos, range.end).trim()
) || [],
};
publicMethods.push(method);
}
}
}
});
return publicMethods;
};
let fileNames = [];
let exportTypes = [];
fs.readdirSync(resourcesFolder).forEach((file) => {
let sources = [];
const fileName =
file.replace(".ts", "").charAt(0).toUpperCase() +
file.replace(".ts", "").slice(1);
fileNames.push(fileName);
const methods = visit(path.resolve(resourcesFolder, file));
const methodNames = methods.map((method) => method.name);
const types = methods.reduce(
(acc, curr) =>
acc.concat(
curr.requests.map((item) => item.type),
curr.responseType
),
[]
);
const filteredTypes = [
...new Set(
types
.filter((type) => !isPrimitiveType(type))
.filter((type) => !isViemType(type))
),
];
exportTypes.push(...filteredTypes);
sources.push(
ejs.render(resourceTemplate.startTemplate, {
types: [filteredTypes],
name: fileName,
methodNames,
viemTypes: [...new Set(types.filter((type) => isViemType(type)))],
})
);
const methodTemplates = methods.map((method) => {
return ejs.render(resourceTemplate.methodTemplate, {
method: method,
fileName: file.replace(".ts", ""),
comments: method.comments,
});
});

sources = sources.concat(
methodTemplates,
ejs.render(resourceTemplate.endTemplate, { methodNames, name: fileName })
);
fs.writeFileSync(`src/resources/use${fileName}.ts`, sources.join("\n"));
});
const indexSource = ejs.render(indexTemplate, {
resources: fileNames,
types: exportTypes,
});
fs.writeFileSync("src/index.ts", indexSource);

console.log("👍👍 React SDK templates generated successfully!");
13 changes: 13 additions & 0 deletions packages/react-sdk/generator/templates/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const indexTemplate = `
export { StoryProvider } from "./StoryProtocolContext";
export { getPermissionSignature, AccessPermission, PIL_TYPE, } from "@story-protocol/core-sdk";
export type { PermissionSignatureRequest, StoryConfig, SupportedChainIds,
<%types.forEach((type,index)=>{%>
<%=type%><%=index === types.length - 1 ? '' : ','%>
<%})%>
} from "@story-protocol/core-sdk";
<% resources.forEach((resource) => {%>
export { default as use<%=resource %> } from "./resources/use<%=resource %>";
<%})%>
`;
module.exports = indexTemplate;
44 changes: 44 additions & 0 deletions packages/react-sdk/generator/templates/resource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const methodTemplate = `<%=comments%>\nconst <%=method.name %> = async (<% method.requests.forEach((item, index)=> { %>
<%= item.name %>: <%= item.type %><%= index === method.requests.length - 1 ? '' : ',' %>
<% }); %>): Promise<<%- method.responseType %>> => {
try {
setLoadings((prev) => ({ ...prev, <%=method.name %>: true }));
setErrors((prev) => ({ ...prev, <%=method.name %>: null }));
const response = await client.<%= fileName%>.<%=method.name %>(<% method.requests.forEach((item,index)=>{%>
<%=item.name %><%=index === method.requests.length - 1 ? '' : ',' %>
<% })%>);
setLoadings((prev ) => ({ ...prev, <%=method.name %>: false }));
return response;
}catch(e){
if(e instanceof Error){
setErrors((prev) => ({ ...prev, <%=method.name %>: e.message }));
setLoadings((prev) => ({ ...prev, <%=method.name %>: false }));
}
throw new Error(\`unhandled error type\`);
}
};
`;

const startTemplate = `import { <% types.forEach((type,index)=>{%>\n<%=type %><%= index===types.length-1?'':','%><%})%>
} from "@story-protocol/core-sdk";
<% if (viemTypes.length > 0) { %>
import { <% viemTypes.forEach((type, index) => { %>\n<%= type %><%= index === viemTypes.length - 1 ? '' : ',' %><% }) %>
} from "viem";
<% } %>
import { useState } from "react";
import { useStoryContext } from "../StoryProtocolContext";
const use<%=name %> = () => {
const client = useStoryContext();
const [loadings,setLoadings] = useState<Record<string,boolean>>({<% methodNames.forEach((name,index)=>{%><%=name %>: false<%=index === methodNames.length - 1 ? '' : ',' %> <%})%>});
const [errors,setErrors] = useState<Record<string,string|null>>({ <% methodNames.forEach((name,index)=>{%><%=name %>: null<%=index === methodNames.length - 1 ? '' : ',' %><%})%> });
`;

const endTemplate = `return {
loadings,
errors,
<% methodNames.forEach((name,index)=>{%><%=name %><%=index === methodNames.length - 1 ? '' : ',' %>
<%})%>
};}\nexport default use<%=name %>;`;

module.exports = { startTemplate, endTemplate, methodTemplate };
75 changes: 75 additions & 0 deletions packages/react-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"name": "@story-protocol/react-sdk",
"version": "1.0.0-rc.14",
"description": "The Story Protocol React SDK",
"main": "dist/story-protocol-react-sdk.cjs.js",
"module": "dist/story-protocol-react-sdk.esm.js",
"exports": {
".": {
"module": "./dist/story-protocol-react-sdk.esm.js",
"default": "./dist/story-protocol-react-sdk.cjs.js"
},
"./package.json": "./package.json"
},
"scripts": {
"generate": "node ./generator/index.js && npm run fix",
"build": "pnpm run fix && preconstruct build",
"fix": "pnpm run format:fix && pnpm run lint:fix",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"lint:fix": "pnpm run lint --fix",
"lint": "eslint ./src",
"tsc": "tsc --noEmit"
},
"sideEffects": false,
"files": [
"dist/**/*"
],
"preconstruct": {
"entrypoints": [
"index.ts"
],
"exports": true,
"externals": [
"react",
"@story-protocol/core-sdk"
]
},
"keywords": [
"story-protocol",
"react",
"sdk",
"react hooks"
],
"babel": {
"presets": [
"@babel/preset-env",
"@babel/preset-typescript",
[
"@babel/preset-react",
{
"runtime": "automatic"
}
]
]
},
"license": "MIT",
"dependencies": {
"@story-protocol/core-sdk": "1.0.0-rc.14",
"react": "^18.3.1",
"viem": "^2.8.12",
"@types/react": "^18.3.3"
},
"devDependencies": {
"@babel/preset-env": "^7.22.20",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.23.0",
"@preconstruct/cli": "^2.8.1",
"@story-protocol/eslint-config": "workspace:*",
"@story-protocol/prettier-config": "workspace:*",
"@story-protocol/tsconfig": "workspace:*",
"ts-node": "^10.9.1",
"typescript": "^5.4.5",
"ejs": "^3.1.10"
}
}
20 changes: 20 additions & 0 deletions packages/react-sdk/src/StoryProtocolContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createContext, useContext, ReactNode } from "react";
import { StoryClient, StoryConfig } from "@story-protocol/core-sdk";

type Props = {
config: StoryConfig;
children: ReactNode;
};

const StoryContext = createContext<StoryClient>({} as StoryClient);

const StoryProvider = ({ config, children }: Props) => {
const client = StoryClient.newClient(config);
return (
<StoryContext.Provider value={client}>{children}</StoryContext.Provider>
);
};
const useStoryContext = (): StoryClient => {
return useContext(StoryContext);
};
export { useStoryContext, StoryProvider };
Loading

0 comments on commit 003e6df

Please sign in to comment.