├── 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 | 2 | 5 | -------------------------------------------------------------------------------- /public/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------