Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React sdk v2 #2

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
link-workspace-packages: false
8 changes: 8 additions & 0 deletions packages/react-sdk/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
EXPERIMENTAL_useProjectService: true,
},
extends: ["@story-protocol/eslint-config"],
};
78 changes: 78 additions & 0 deletions packages/react-sdk/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# 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.

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
```

- Import the provider in your React application

```typescript
import { StoryProvider } from "@story-protocol/react-sdk";
const client = StoryClient.newClient(config);
<StoryProvider client={client}>
<App />
</StoryProvider>;
```

- Use the hooks in your components

```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.
124 changes: 124 additions & 0 deletions packages/react-sdk/generator/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
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!");
console.log();
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);
const checker = 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 = [];
const methodSignature = 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 = [];
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
),
[]
);

sources.push(
ejs.render(resourceTemplate.startTemplate, {
types: [
...new Set(
types
.filter((type) => !isPrimitiveType(type))
.filter((type) => !isViemType(type))
),
],
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 });
fs.writeFileSync("src/index.ts", indexSource);

console.log("👍👍 React SDK templates generated successfully!");
7 changes: 7 additions & 0 deletions packages/react-sdk/generator/templates/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const indexTemplate = `
export { StoryProvider } from "./StoryProtocolContext";
<% 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": {
"generator": "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",
"ejs": "^3.1.10",
"react": "^18.3.1",
"viem": "^2.8.12"
},
"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:*",
"@types/react": "^18.3.3",
"ts-node": "^10.9.1",
"typescript": "^5.4.5"
}
}
15 changes: 15 additions & 0 deletions packages/react-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export { StoryProvider } from "./StoryProtocolContext";

export { default as useDispute } from "./resources/useDispute";

export { default as useIpAccount } from "./resources/useIpAccount";

export { default as useIpAsset } from "./resources/useIpAsset";

export { default as useLicense } from "./resources/useLicense";

export { default as useNftClient } from "./resources/useNftClient";

export { default as usePermission } from "./resources/usePermission";

export { default as useRoyalty } from "./resources/useRoyalty";
Loading