├── src
├── react-app-env.d.ts
├── setupTests.ts
├── App.test.tsx
├── index.css
├── reportWebVitals.ts
├── index.tsx
├── routes
│ ├── launchIcon.svg
│ ├── styles.tsx
│ ├── LandingPage.tsx
│ └── Send.tsx
├── App.css
├── App.tsx
├── logo.svg
└── StateContext.tsx
├── .babelrc
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── manifest.json
├── logo-dark.svg
├── logo-light.svg
└── index.html
├── declarations.d.ts
├── tsconfig.json
├── .gitignore
├── .eslintrc.js
├── README.md
├── package.json
└── webpack.config.js
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"]
3 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zkemail/oauth-demo-ui/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zkemail/oauth-demo-ui/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zkemail/oauth-demo-ui/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/declarations.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svg' {
2 | const content: any;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "commonjs",
5 | "strict": true,
6 | "jsx": "react",
7 | "esModuleInterop": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true
10 | },
11 | "include": [
12 | "src/**/*", "declarations.d.ts"
13 | ],
14 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | .env
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 | /dist
9 |
10 | # testing
11 | /coverage
12 |
13 | # production
14 | /build
15 |
16 | # misc
17 | .DS_Store
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | bun.lock
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 | * {
16 | box-sizing: border-box;
17 | }
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | },
6 | extends: [
7 | 'eslint:recommended',
8 | 'plugin:react/recommended',
9 | ],
10 | parserOptions: {
11 | ecmaFeatures: {
12 | jsx: true,
13 | },
14 | ecmaVersion: 12,
15 | sourceType: 'module',
16 | },
17 | plugins: [
18 | 'react',
19 | 'react-hooks',
20 | ],
21 | rules: {
22 | 'react/react-in-jsx-scope': 'off',
23 | },
24 | };
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/logo-light.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | const root = ReactDOM.createRoot(
8 | document.getElementById('root') as HTMLElement
9 | );
10 | root.render(
11 |
12 |
13 |
14 | );
15 |
16 | // If you want to start measuring performance in your app, pass a function
17 | // to log results (for example: reportWebVitals(console.log))
18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19 | reportWebVitals();
20 |
--------------------------------------------------------------------------------
/src/routes/launchIcon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Oauth Email Wallet Demo
2 |
3 | Authorize scoped access to your email wallet account controlled by ZK Email proofs, via just 3 lines of code in a frontend SDK. This demo site shows how to integrate it with a live deployed example.
4 |
5 | ## Installation
6 | ```
7 | npm install @zk-email/oauth-sdk
8 | ```
9 |
10 | ## Usage
11 |
12 | To learn more about how to integrate, check out [our OAuth docs](https://docs.zk.email/login-with-zk-email-oauth-api).
13 |
14 | ## Link ts-sdk
15 |
16 | ```
17 | git clone git@github.com:zkemail/email-wallet.git
18 | cd email-wallet
19 | git checkout feat/oauth-mvp
20 | cd email-wallet/packages/ts-sdk
21 | npm link
22 |
23 | cd {THIS_REPO}
24 | npm link @zk-email/ts-sdk
25 | ```
26 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/routes/styles.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from "react";
2 |
3 | export const styles: { [key: string]: CSSProperties } = {
4 | input: {
5 | margin: "0",
6 | padding: "0.625rem",
7 | fontSize: "1rem",
8 | borderRadius: "4px",
9 | border: "1px solid #ccc",
10 | width: "100%",
11 | height: "max-content",
12 | },
13 | button: {
14 | border: "none",
15 | color: 'black',
16 | borderRadius: "4px",
17 | padding: "0.75rem",
18 | fontSize: "1rem",
19 | fontWeight: 700,
20 | cursor: "pointer",
21 | },
22 | transaction: {
23 | border: "1px solid #ccc",
24 | padding: "1rem",
25 | cursor: "pointer",
26 | borderRadius: 4,
27 | },
28 | };
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | // src/App.tsx
2 | import React from "react";
3 | // import WaitingPage from './routes/WaitingPage';
4 | import LandingPage from "./routes/LandingPage";
5 | import SendPage from "./routes/Send";
6 | import { HashRouter, Routes, Route } from "react-router-dom";
7 | import { StateProvider } from "./StateContext";
8 | import { CssVarsProvider, extendTheme } from "@mui/joy/styles";
9 |
10 | const theme = extendTheme({
11 | colorSchemes: {
12 | light: {
13 | palette: {
14 | primary: {
15 | 50: '#FACC15',
16 | 100: '#FACC1500',
17 | 200: '#FACC15',
18 | 300: '#FACC15',
19 | 400: '#FACC15',
20 | 500: '#FACC15',
21 | 600: '#FACC15',
22 | 700: '#FACC15',
23 | 800: '#FACC15',
24 | 900: '#FACC15',
25 | },
26 | },
27 | },
28 | },
29 |
30 | });
31 |
32 | const App: React.FC = () => {
33 | return (
34 |
35 |
36 |
37 |
38 |
39 | } />
40 | } />
41 |
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default App;
50 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
18 |
27 | Email Oauth Demo
28 |
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oauth-demo-ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.11.4",
7 | "@emotion/styled": "^11.11.5",
8 | "@mui/joy": "^5.0.0-beta.47",
9 | "@testing-library/jest-dom": "^5.17.0",
10 | "@testing-library/react": "^13.4.0",
11 | "@testing-library/user-event": "^13.5.0",
12 | "@types/jest": "^27.5.2",
13 | "@types/node": "^16.18.101",
14 | "@types/react": "^18.3.3",
15 | "@types/react-dom": "^18.3.0",
16 | "@zk-email/oauth-sdk": "^0.1.3",
17 | "dotenv": "^16.4.5",
18 | "file-loader": "^6.2.0",
19 | "react": "^18.3.1",
20 | "react-dom": "^18.3.1",
21 | "react-router-dom": "^6.24.1",
22 | "reverse-mirage": "^1.1.0",
23 | "svg-inline-loader": "^0.8.0",
24 | "typescript": "^5.5.3",
25 | "viem": "^2.17.2",
26 | "wagmi": "^2.10.9",
27 | "web-vitals": "^2.1.4"
28 | },
29 | "scripts": {
30 | "start": "webpack serve --mode development",
31 | "build": "webpack --mode production",
32 | "test": "echo \"Error: no test specified\" && exit 1"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | },
52 | "devDependencies": {
53 | "@babel/core": "^7.24.7",
54 | "@babel/preset-env": "^7.24.7",
55 | "@babel/preset-react": "^7.24.7",
56 | "babel-loader": "^9.1.3",
57 | "clean-webpack-plugin": "^4.0.0",
58 | "css-loader": "^7.1.2",
59 | "eslint": "^8.57.0",
60 | "eslint-plugin-react": "^7.34.3",
61 | "eslint-plugin-react-hooks": "^4.6.2",
62 | "html-webpack-plugin": "^5.6.0",
63 | "style-loader": "^4.0.0",
64 | "ts-loader": "^9.5.1",
65 | "webpack": "^5.92.1",
66 | "webpack-cli": "^5.1.4",
67 | "webpack-dev-server": "^5.0.4"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const HtmlWebpackPlugin = require("html-webpack-plugin");
3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin");
4 | const { DefinePlugin } = require("webpack");
5 | const dotenv = require("dotenv");
6 | dotenv.config();
7 |
8 | module.exports = {
9 | entry: "./src/index.tsx",
10 | output: {
11 | filename: "bundle.js",
12 | path: path.resolve(__dirname, "dist"),
13 | publicPath: "/",
14 | },
15 | mode: "development",
16 | devServer: {
17 | static: {
18 | directory: path.join(__dirname, "public"),
19 | },
20 | historyApiFallback: true,
21 | // proxy: {
22 | // '/api': {
23 | // target: process.env.REACT_APP_RELAYER_HOST,
24 | // changeOrigin: true,
25 | // pathRewrite: {
26 | // '^/api': '/api',
27 | // },
28 | // // onProxyReq: (proxyReq, req, res) => {
29 | // // proxyReq.setHeader('Origin', 'https://frontend.com');
30 | // // },
31 | // }
32 | // }
33 | // before: function (app, server, compiler) {
34 | // app.use((req, res, next) => {
35 | // res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
36 | // res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
37 | // res.setHeader("Access-Control-Allow-Origin", "*");
38 | // next();
39 | // });
40 | // },
41 | },
42 | module: {
43 | rules: [
44 | {
45 | test: /\.(ts|tsx)$/, // Add TypeScript support
46 | exclude: /node_modules/,
47 | use: "ts-loader", // Use ts-loader for TypeScript files
48 | },
49 | {
50 | test: /\.(js|jsx)$/,
51 | exclude: /node_modules/,
52 | use: {
53 | loader: "babel-loader",
54 | },
55 | },
56 | {
57 | test: /\.css$/,
58 | use: ["style-loader", "css-loader"],
59 | },
60 | {
61 | test: /\.svg$/,
62 | use: "file-loader", // Use url-loader for SVG files
63 | },
64 | ],
65 | },
66 | resolve: {
67 | extensions: [".ts", ".tsx", ".js", ".jsx"], // Add .ts and .tsx extensions
68 | },
69 | plugins: [
70 | new CleanWebpackPlugin(),
71 | new HtmlWebpackPlugin({
72 | template: "./public/index.html",
73 | }),
74 | new DefinePlugin({
75 | "process.env": JSON.stringify(process.env),
76 | }),
77 | ],
78 | };
79 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/StateContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, ReactNode, useEffect } from 'react';
2 | import { OauthClient } from "@zk-email/oauth-sdk";
3 | import { Address, GetContractReturnType, PrivateKeyAccount, PublicClient, createPublicClient, WalletClient, getContract, encodePacked, http, } from 'viem'
4 | import { baseSepolia, mainnet, base } from "viem/chains";
5 |
6 | type StateContextType = {
7 | userEmailAddr: string,
8 | setUserEmailAddr: (userEmailAddr: string) => void,
9 | username: string,
10 | setUsername: (username: string) => void,
11 | oauthClient: OauthClient,
12 | setOauthClient: (oauthClient: OauthClient) => void,
13 | requestId: number | null,
14 | setRequestId: (requestId: number | null) => void,
15 | pageState: PageState,
16 | setPageState: (pageState: PageState) => void,
17 | };
18 |
19 | export type OauthClientCache = {
20 | userEmailAddr: string,
21 | username: string,
22 | userWalletAddr: Address,
23 | ephePrivateKey: `0x${string}`,
24 | epheAddrNonce: string,
25 | }
26 |
27 | export enum PageState {
28 | landing = 0,
29 | waiting = 1,
30 | send = 2,
31 |
32 | }
33 |
34 | const StateContext = createContext(undefined);
35 | export const StateProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
36 | const coreAddress = process.env.REACT_APP_CORE_ADDRESS || '';
37 | const oauthAddress = process.env.REACT_APP_OAUTH_ADDRESS || '';
38 | const relayerHost = process.env.REACT_APP_RELAYER_HOST || '';
39 | const publicClient = createPublicClient({
40 | chain: baseSepolia,
41 | transport: http("https://sepolia.base.org"),
42 | });
43 | const cachedOauthClientStr = localStorage.getItem('oauthClient');
44 | const cachedOauthClient: OauthClientCache | null = cachedOauthClientStr ? JSON.parse(cachedOauthClientStr) as OauthClientCache : null;
45 | console.log(cachedOauthClient);
46 | const [userEmailAddr, setUserEmailAddr] = useState(cachedOauthClient !== null ? cachedOauthClient.userEmailAddr : '');
47 | const [username, setUsername] = useState(cachedOauthClient !== null ? cachedOauthClient.username : '');
48 | // const [isFilled, setIsFilled] = useState(false);
49 | const oauthClientInit = cachedOauthClient !== null ? new OauthClient(publicClient, coreAddress as Address, oauthAddress as Address, relayerHost, cachedOauthClient.userEmailAddr, cachedOauthClient.userWalletAddr, cachedOauthClient.ephePrivateKey, cachedOauthClient.epheAddrNonce) : new OauthClient(publicClient, coreAddress as Address, oauthAddress as Address, relayerHost);
50 | const [oauthClient, setOauthClient] = useState>(oauthClientInit);
51 | const [requestId, setRequestId] = useState(null);
52 | // const [isActivated, setIsActivated] = useState(cachedOauthClient !== null);
53 | const [pageState, setPageState] = useState(cachedOauthClient !== null ? PageState.send : PageState.landing);
54 |
55 | // useEffect(() => {
56 | // const setup = async () => {
57 | // if (!isFilled) {
58 | // return;
59 | // }
60 | // const requestId = await oauthClient?.setup(userEmailAddr, username, null, [[10, "TEST"]]);
61 | // setOauthClient(oauthClient);
62 | // setRequestId(requestId);
63 | // setIsFilled(false);
64 | // };
65 | // setup();
66 | // }, [isFilled, userEmailAddr, username]);
67 |
68 |
69 | // useEffect(() => {
70 | // const waiting = async () => {
71 | // if (requestId !== null) {
72 | // console.log(requestId);
73 | // await oauthClient?.waitEpheAddrActivated(requestId);
74 | // setOauthClient(oauthClient);
75 | // setRequestId(null);
76 | // setPageState(PageState.send);
77 | // const newCache: OauthClientCache = {
78 | // userEmailAddr,
79 | // username,
80 | // userWalletAddr: oauthClient.getWalletAddress() as Address,
81 | // ephePrivateKey: oauthClient.getEphePrivateKey(),
82 | // epheAddrNonce: oauthClient.getEpheAddrNonce() as string,
83 | // };
84 | // localStorage.setItem('oauthClient', JSON.stringify(newCache));
85 | // }
86 | // }
87 | // waiting();
88 | // }, [requestId]);
89 |
90 | return (
91 |
92 | {children}
93 |
94 | );
95 | };
96 |
97 | export const useAppState = () => {
98 | const context = useContext(StateContext);
99 | if (!context) {
100 | throw new Error('useAppState must be used within a StateProvider');
101 | }
102 | return context;
103 | };
--------------------------------------------------------------------------------
/src/routes/LandingPage.tsx:
--------------------------------------------------------------------------------
1 | // src/LandingPage.tsx
2 | import React, { useState, CSSProperties, useEffect } from "react";
3 | import { Outlet, Link, useNavigate } from "react-router-dom";
4 | import { useAppState, PageState } from "../StateContext";
5 | import { Button, Grid, Tab, TabList, Tabs, Typography } from "@mui/joy";
6 | import { styles } from "./styles";
7 |
8 | const emailRegex: RegExp = /^[A-Za-z0-9!#$%&'*+=?\\-\\^_`{|}~./@]+@[A-Za-z0-9.\\-]+$/;
9 |
10 | const LandingPage: React.FC = () => {
11 | // const [email, setEmail] = useState('');
12 | // const [username, setUsername] = useState('');
13 | const [selectedOption, setSelectedOption] = useState<
14 | "signup" | "signin" | null
15 | >(`signup`);
16 | const [isFilled, setIsFilled] = useState(false);
17 | const {
18 | userEmailAddr,
19 | setUserEmailAddr,
20 | username,
21 | setUsername,
22 | pageState,
23 | setPageState,
24 | oauthClient,
25 | setOauthClient,
26 | requestId,
27 | setRequestId,
28 | } = useAppState();
29 | const navigate = useNavigate();
30 |
31 | const handleEmailChange = (e: React.ChangeEvent) => {
32 | setUserEmailAddr(e.target.value);
33 | };
34 |
35 | const handleUsernameChange = (e: React.ChangeEvent) => {
36 | setUsername(e.target.value);
37 | };
38 |
39 | const handleOptionClick = (option: "signup" | "signin") => {
40 | setSelectedOption(option);
41 | };
42 |
43 | useEffect(() => {
44 | setSelectedOption("signup");
45 | setUserEmailAddr("");
46 | setUsername("");
47 | }, []);
48 |
49 | const handleNextClick = () => {
50 | setIsFilled(true);
51 | setPageState(PageState.waiting);
52 | // navigate('/send');
53 | // const requestId = await oauthClient?.setup(email, username, null, null);
54 | // if (requestId != null) {
55 | // setRequestId(requestId);
56 | // }
57 | };
58 |
59 | useEffect(() => {
60 | const setup = async () => {
61 | if (!isFilled) {
62 | return;
63 | }
64 | console.log(userEmailAddr, username, null, [[10, "TEST"]]);
65 | const requestId = await oauthClient?.setup(
66 | userEmailAddr,
67 | username,
68 | null,
69 | [[10, "TEST"]]
70 | );
71 | setOauthClient(oauthClient);
72 | setRequestId(requestId);
73 | setIsFilled(false);
74 | };
75 | setup();
76 | }, [isFilled, userEmailAddr, username]);
77 |
78 | useEffect(() => {
79 | if (pageState === PageState.waiting || pageState === PageState.send) {
80 | navigate("/send");
81 | }
82 | }, [pageState]);
83 |
84 | const isFormValid = () => {
85 | if (selectedOption === "signup") {
86 | return userEmailAddr !== "" && emailRegex.test(userEmailAddr) && username !== "" && !emailRegex.test(username);
87 | } else {
88 | return userEmailAddr !== "" && emailRegex.test(userEmailAddr);
89 | }
90 | };
91 |
92 | // const styles: { [key: string]: CSSProperties } = {
93 | // container: {
94 | // display: 'flex',
95 | // flexDirection: 'column',
96 | // alignItems: 'center',
97 | // justifyContent: 'center',
98 | // height: '100vh',
99 | // backgroundColor: '#f0f0f0',
100 | // },
101 | // title: {
102 | // marginBottom: '20px',
103 | // },
104 | // input: {
105 | // padding: '10px',
106 | // fontSize: '16px',
107 | // marginBottom: '10px',
108 | // borderRadius: '5px',
109 | // border: '1px solid #ccc',
110 | // },
111 | // button: {
112 | // padding: '10px 20px',
113 | // fontSize: '16px',
114 | // borderRadius: '5px',
115 | // border: 'none',
116 | // backgroundColor: '#007bff',
117 | // color: '#fff',
118 | // cursor: 'pointer',
119 | // margin: '5px',
120 |
121 | // },
122 | // inactiveButton: {
123 | // backgroundColor: '#cccccc',
124 | // },
125 | // disabledButton: {
126 | // backgroundColor: '#cccccc',
127 | // cursor: 'not-allowed',
128 | // },
129 | // link: {
130 | // color: '#fff',
131 | // textDecoration: 'none',
132 | // },
133 | // };
134 |
135 | return (
136 |
142 |
157 |
158 |
159 | Email Oauth Demo
160 |
161 |
162 |
163 |
164 |
167 | setSelectedOption(value as "signup" | "signin" | null)
168 | }
169 | >
170 |
171 | Sign-up
172 | Sign-in
173 |
174 |
175 |
176 | {/*
177 |
185 |
186 |
187 |
195 | */}
196 |
197 |
198 |
199 |
206 |
207 |
208 | {selectedOption === "signup" && (
209 |
216 | )}
217 |
218 |
219 |
220 |
228 |
229 |
230 |
231 | );
232 | };
233 |
234 | export default LandingPage;
235 |
--------------------------------------------------------------------------------
/src/routes/Send.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { OauthClientCache, PageState, useAppState } from "../StateContext";
3 | import { Address, encodeFunctionData, getAddress, createPublicClient, http } from "viem";
4 | import { erc20Abi } from "viem";
5 | import CircularProgress from "@mui/joy/CircularProgress";
6 | import List from "@mui/joy/List";
7 | import ListItem from "@mui/joy/ListItem";
8 | import { Button, Grid, Typography } from "@mui/joy";
9 | import { styles } from "./styles";
10 | import { useNavigate } from "react-router-dom";
11 | import { OauthClient } from "@zk-email/oauth-sdk";
12 | import { baseSepolia, mainnet, base } from "viem/chains";
13 | import LaunchIcon from './launchIcon.svg'
14 |
15 | const SendPage: React.FC = () => {
16 | const navigate = useNavigate();
17 |
18 | const {
19 | oauthClient,
20 | setOauthClient,
21 | pageState,
22 | setPageState,
23 | requestId,
24 | setRequestId,
25 | userEmailAddr,
26 | setUserEmailAddr,
27 | username,
28 | setUsername,
29 | } = useAppState();
30 | const [amountStr, setAmountStr] = useState("");
31 | const [amount, setAmount] = useState(0n);
32 | const [token, setToken] = useState("TEST");
33 | const [to, setTo] = useState("");
34 | const decimals = 18;
35 | const baseAmount = 10n ** BigInt(decimals);
36 | const [balanceOfTest, setBalanceOfTest] = useState(0n);
37 | const [allowanceOfTest, setAllowanceOfTest] = useState(
38 | 0n
39 | );
40 | const [loading, setLoading] = useState(false);
41 | // const [isExecuting, setIsExecuting] = useState(false);
42 | const [txInfos, setTxInfos] = useState<[string, string, string][]>([]);
43 |
44 | const testTokenAddr = process.env.REACT_APP_TEST_TOKEN as Address | null;
45 | if (!testTokenAddr) {
46 | throw new Error("REACT_APP_TEST_TOKEN is not set");
47 | }
48 |
49 | const handleAmountChange = (e: React.ChangeEvent) => {
50 | setAmountStr(e.target.value);
51 | console.log("amountStr:", amountStr);
52 | setAmount(_amountStrToBigint(e.target.value, decimals));
53 | };
54 |
55 | const handleTokenChange = (e: React.ChangeEvent) => {
56 | setToken(e.target.value);
57 | };
58 |
59 | const handleToChange = (e: React.ChangeEvent) => {
60 | setTo(e.target.value);
61 | };
62 |
63 | const handleSendClick = async () => {
64 | setLoading(true);
65 | console.log("Sending", { amount, token, to });
66 | let toAddress;
67 | try {
68 | toAddress = getAddress(to);
69 | } catch (e) {
70 | console.log("Incorrect to address. Please check the value and retry");
71 | return;
72 | }
73 | const data = encodeFunctionData({
74 | abi: erc20Abi,
75 | functionName: "transfer",
76 | args: [toAddress as Address, amount],
77 | });
78 | const txHash = await oauthClient?.oauthExecuteTx(
79 | testTokenAddr,
80 | data,
81 | 0n,
82 | amount
83 | );
84 | console.log("txHash:", txHash);
85 | setTxInfos([...txInfos, [txHash, amountStr, toAddress]]);
86 | const newBalance = balanceOfTest - amount;
87 | setBalanceOfTest(newBalance);
88 | const newAllowance = allowanceOfTest - amount;
89 | setAllowanceOfTest(newAllowance);
90 | setLoading(false);
91 | };
92 |
93 | useEffect(() => {
94 | const setupBalances = async () => {
95 | if (!oauthClient?.userWallet?.address) {
96 | return;
97 | }
98 | if (pageState !== PageState.send) {
99 | return;
100 | }
101 | const balance = await oauthClient?.publicClient.readContract({
102 | address: testTokenAddr!,
103 | abi: erc20Abi,
104 | functionName: "balanceOf",
105 | args: [oauthClient?.userWallet?.address],
106 | });
107 | setBalanceOfTest(balance);
108 | const allowance = await oauthClient.getTokenAllowance(testTokenAddr);
109 | setAllowanceOfTest(allowance);
110 | };
111 | setupBalances();
112 | }, [pageState]);
113 |
114 | useEffect(() => {
115 | const waiting = async () => {
116 | if (requestId === null) {
117 | console.log("request id is null");
118 | return;
119 | }
120 | console.log(requestId);
121 | await oauthClient?.waitEpheAddrActivated(requestId!);
122 | setOauthClient(oauthClient);
123 | setRequestId(null);
124 | setPageState(PageState.send);
125 | const newCache: OauthClientCache = {
126 | userEmailAddr,
127 | username,
128 | userWalletAddr: oauthClient.getWalletAddress() as Address,
129 | ephePrivateKey: oauthClient.getEphePrivateKey(),
130 | epheAddrNonce: oauthClient.getEpheAddrNonce() as string,
131 | };
132 | console.log(newCache);
133 | localStorage.setItem("oauthClient", JSON.stringify(newCache));
134 | };
135 | waiting();
136 | }, [requestId]);
137 |
138 | const handleLogout = () => {
139 | localStorage.removeItem("oauthClient");
140 | setUserEmailAddr('');
141 | setUsername('');
142 | const coreAddress = process.env.REACT_APP_CORE_ADDRESS || '';
143 | const oauthAddress = process.env.REACT_APP_OAUTH_ADDRESS || '';
144 | const relayerHost = process.env.REACT_APP_RELAYER_HOST || '';
145 | const publicClient = createPublicClient({
146 | chain: baseSepolia,
147 | transport: http("https://sepolia.base.org"),
148 | });
149 | setOauthClient(new OauthClient(publicClient, coreAddress as Address, oauthAddress as Address, relayerHost));
150 | setRequestId(null);
151 | setPageState(PageState.landing);
152 | navigate("/");
153 | };
154 |
155 | if (pageState !== PageState.send) {
156 | return (
157 |
170 |
171 |
Loading until your sign-up/sign-in is completed.
172 |
173 | );
174 | }
175 |
176 | return (
177 |
183 |
193 |
194 |
195 |
210 |
211 |
212 | Wallet Information
213 |
214 |
215 |
216 | Wallet address:{" "}
217 | {`${oauthClient?.userWallet?.address}` ?? "Not available"}
218 |
219 |
220 |
221 |
222 | Balance: {_bigIntToAmountStr(balanceOfTest, decimals)} TEST
223 |
224 |
225 |
226 |
227 | Remaining Allowance:{" "}
228 | {_bigIntToAmountStr(allowanceOfTest, decimals)} TEST
229 |
230 |
231 |
232 |
233 |
234 |
241 |
242 |
243 |
250 |
251 |
252 |
259 |
260 |
261 |
269 |
270 |
271 | {txInfos.length ? (
272 |
273 |
274 | Executed Transactions:
275 |
276 |
277 |
278 |
279 | {txInfos.map(([txHash, amount, to], index) => (
280 |
281 |
286 |
287 |
293 | Sent {amount} "TEST" to {to}
294 |
295 |
296 |
297 |
298 |
299 | ))}
300 |
301 |
302 |
303 | ) : null}
304 |
305 |
306 | );
307 | };
308 |
309 | function _amountStrToBigint(input: string, decimals: number): bigint {
310 | const [whole, fraction = ""] = input.split(".");
311 | const base = BigInt(whole) * 10n ** 18n;
312 | console.log(`whole: ${whole}, fraction: ${fraction}`);
313 | const exponent = BigInt(fraction) * 10n ** BigInt(18 - fraction.length);
314 | console.log("base:", base);
315 | console.log("exponent:", exponent);
316 | return base + exponent;
317 | }
318 |
319 | function _bigIntToAmountStr(input: bigint, decimals: number): string {
320 | const base = input.toString();
321 | const whole = base.slice(0, -decimals) || "0";
322 | const fraction = base.slice(-decimals).replace(/0+$/, "");
323 | if (fraction.length === 0) return whole;
324 | else return `${whole}.${fraction}`;
325 | }
326 |
327 | export default SendPage;
328 |
--------------------------------------------------------------------------------