diff --git a/package-lock.json b/package-lock.json index 7d9a888c..b1debba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "11.11.1", "@types/dom-screen-wake-lock": "1.0.1", + "buffer": "6.0.3", "ethers": "6.6.7", "i18next": "23.4.1", "jest-environment-jsdom": "29.6.2", @@ -4393,6 +4394,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -4544,6 +4564,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -7379,6 +7422,25 @@ "postcss": "^8.1.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -12935,7 +12997,7 @@ "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.2", - "semver": "7.5.4" + "semver": "^6.3.0" } }, "@babel/eslint-parser": { @@ -12946,7 +13008,7 @@ "requires": { "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", "eslint-visitor-keys": "^2.1.0", - "semver": "7.5.4" + "semver": "^6.3.0" }, "dependencies": { "eslint-visitor-keys": { @@ -12997,7 +13059,7 @@ "@babel/helper-validator-option": "^7.21.0", "browserslist": "^4.21.3", "lru-cache": "^5.1.1", - "semver": "7.5.4" + "semver": "^6.3.0" } }, "@babel/helper-create-class-features-plugin": { @@ -13014,7 +13076,7 @@ "@babel/helper-replace-supers": "^7.21.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/helper-split-export-declaration": "^7.18.6", - "semver": "7.5.4" + "semver": "^6.3.0" } }, "@babel/helper-create-regexp-features-plugin": { @@ -13025,7 +13087,7 @@ "requires": { "@babel/helper-annotate-as-pure": "^7.18.6", "regexpu-core": "^5.3.1", - "semver": "7.5.4" + "semver": "^6.3.0" } }, "@babel/helper-define-polyfill-provider": { @@ -13039,7 +13101,7 @@ "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2", - "semver": "7.5.4" + "semver": "^6.1.2" } }, "@babel/helper-environment-visitor": { @@ -13975,7 +14037,7 @@ "babel-plugin-polyfill-corejs2": "^0.3.3", "babel-plugin-polyfill-corejs3": "^0.6.0", "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "7.5.4" + "semver": "^6.3.0" } }, "@babel/plugin-transform-shorthand-properties": { @@ -14136,7 +14198,7 @@ "babel-plugin-polyfill-corejs3": "^0.6.0", "babel-plugin-polyfill-regenerator": "^0.4.1", "core-js-compat": "^3.25.1", - "semver": "7.5.4" + "semver": "^6.3.0" } }, "@babel/preset-modules": { @@ -15230,7 +15292,7 @@ "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", - "semver": "7.5.4", + "semver": "^7.3.7", "tsutils": "^3.21.0" }, "dependencies": { @@ -15261,7 +15323,7 @@ "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "7.5.4", + "semver": "^7.3.7", "tsutils": "^3.21.0" } }, @@ -15278,7 +15340,7 @@ "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", - "semver": "7.5.4" + "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { @@ -15341,7 +15403,7 @@ "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "7.5.4", + "semver": "^7.3.7", "tsutils": "^3.21.0" } }, @@ -15406,7 +15468,7 @@ "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "7.5.4", + "semver": "^7.3.7", "tsutils": "^3.21.0" } }, @@ -15423,7 +15485,7 @@ "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", - "semver": "7.5.4" + "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { @@ -15455,7 +15517,7 @@ "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "7.5.4", + "semver": "^7.3.7", "tsutils": "^3.21.0" } }, @@ -15472,7 +15534,7 @@ "@typescript-eslint/types": "5.59.2", "@typescript-eslint/typescript-estree": "5.59.2", "eslint-scope": "^5.1.1", - "semver": "7.5.4" + "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { @@ -16009,7 +16071,7 @@ "requires": { "@babel/compat-data": "^7.17.7", "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "7.5.4" + "semver": "^6.1.1" } }, "babel-plugin-polyfill-corejs3": { @@ -16097,6 +16159,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -16218,6 +16285,15 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -16584,7 +16660,7 @@ "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", - "yaml": "2.3.1" + "yaml": "2.2.2" } }, "cross-spawn": { @@ -16618,7 +16694,7 @@ "postcss-modules-scope": "^3.0.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "7.5.4" + "semver": "^7.3.8" } }, "css-minimizer-webpack-plugin": { @@ -17230,7 +17306,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "0.9.3", + "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, @@ -17347,7 +17423,7 @@ "minimatch": "^3.1.2", "object.values": "^1.1.6", "resolve": "^1.22.1", - "semver": "7.5.4", + "semver": "^6.3.0", "tsconfig-paths": "^3.14.1" }, "dependencies": { @@ -17401,7 +17477,7 @@ "minimatch": "^3.1.2", "object.entries": "^1.1.6", "object.fromentries": "^2.0.6", - "semver": "7.5.4" + "semver": "^6.3.0" } }, "eslint-plugin-react": { @@ -17423,7 +17499,7 @@ "object.values": "^1.1.6", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.4", - "semver": "7.5.4", + "semver": "^6.3.0", "string.prototype.matchall": "^4.0.8" }, "dependencies": { @@ -18306,6 +18382,11 @@ "dev": true, "requires": {} }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -18668,7 +18749,7 @@ "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", - "semver": "7.5.4" + "semver": "^6.3.0" } }, "istanbul-lib-report": { @@ -19402,7 +19483,7 @@ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "requires": { - "semver": "7.5.4" + "semver": "^6.0.0" } }, "make-error": { @@ -21625,7 +21706,7 @@ "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", - "semver": "7.5.4", + "semver": "^7.5.3", "yargs-parser": "^21.0.1" } }, @@ -21638,7 +21719,7 @@ "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", - "semver": "7.5.4" + "semver": "^7.3.4" } }, "tsconfig-paths": { @@ -22214,8 +22295,7 @@ "dev": true }, "yaml": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "version": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==" }, "yargs": { diff --git a/package.json b/package.json index 3d33f7e8..0463d237 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "dependencies": { "@emotion/react": "11.11.1", "@types/dom-screen-wake-lock": "1.0.1", + "buffer": "6.0.3", "ethers": "6.6.7", "i18next": "23.4.1", "jest-environment-jsdom": "29.6.2", diff --git a/src/App.tsx b/src/App.tsx index 61813a37..a230ce0c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,6 +38,8 @@ export const App: React.FC = () => { isCallReady, isWeb3Call, setIsWeb3Call, + web3Account, + setWeb3Account, setJwt, setRoomName, setJitsiContext, @@ -53,7 +55,6 @@ export const App: React.FC = () => { return ( -
{ setJwt={setJwt} jitsiContext={jitsiContext} setJitsiContext={setJitsiContext} + web3Account={web3Account} + setWeb3Account={setWeb3Account} /> ) : ( { browser={browserProps} isWeb3Call={isWeb3Call} setIsWeb3Call={setIsWeb3Call} + web3Account={params.isSolana ? web3Account : "ETH"} + setWeb3Account={setWeb3Account} setJwt={setJwt} setRoomName={setRoomName} jitsiContext={jitsiContext} diff --git a/src/components/WelcomeScreen.tsx b/src/components/WelcomeScreen.tsx index 93dd430f..38ce9d19 100644 --- a/src/components/WelcomeScreen.tsx +++ b/src/components/WelcomeScreen.tsx @@ -1,4 +1,5 @@ import React, { DispatchWithoutAction } from "react"; +import { css } from '@emotion/react'; import { useSubscribedStatus } from "../hooks/use-subscribed-status"; import { Background } from "./Background"; import { Footer } from "./Footer"; @@ -15,6 +16,7 @@ import { Web3CTA } from "./web3/Web3CTA"; import { StartCall } from "./web3/StartCall"; import { JitsiContext } from "../jitsi/types"; import { resolveService } from "../services"; +import { Text } from "./Text"; interface Props { onStartCall: DispatchWithoutAction; @@ -23,8 +25,10 @@ interface Props { hasInitialRoomName: boolean; browser: BrowserProperties; isWeb3Call: boolean; + web3Account: "ETH" | "SOL" | null; jitsiContext: JitsiContext; setIsWeb3Call: (isWeb3Call: boolean) => void; + setWeb3Account: (web3Account: "ETH" | "SOL") => void; setJwt: (jwt: string) => void; setRoomName: (roomName: string) => void; setJitsiContext: (context: JitsiContext) => void; @@ -39,6 +43,8 @@ export const WelcomeScreen: React.FC = ({ isWeb3Call, jitsiContext, setIsWeb3Call, + web3Account, + setWeb3Account, setJwt, setRoomName, setJitsiContext, @@ -58,6 +64,15 @@ export const WelcomeScreen: React.FC = ({ } }; + const onClickSolAccount = () => { + setWeb3Account("SOL"); + return; + }; + + const onClickEthAccount = () => { + setWeb3Account("ETH"); + return; + }; const Body: React.FC = () => { if (!hasInitialRoomName && browser.isBrave === false) { return ; @@ -74,13 +89,109 @@ export const WelcomeScreen: React.FC = ({ ); } - if (isWeb3Call) { + if (isWeb3Call && web3Account === null) { + // Define styles using the css prop + const popupContainerStyle = css` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + `; + + const popupContentStyle = css` + background: white; + padding: 20px; + border-radius: 5px; + text-align: center; + `; + + const buttonContainerStyle = css` + margin-top: 20px; + display: flex; + justify-content: center; + `; + + const buttonStyle = css` + margin: 0 10px; + padding: 10px 20px; + border: solid 1px #ccc; + border-radius: 10px; + background-color: transparent; + color: black; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + overflow: hidden; + flex: 1; + `; + + const buttonWrapperStyle = css` + display: flex; + width: 100%; + `; + + const logoStyle = css` + width: 50px; + height: 50px; + margin-bottom: 10px; + border-radius: 50%; + `; + + return ( +
+
+ + Web3 Account + + +
+
+ + +
+
+
+
+ ); + } + if (isWeb3Call && web3Account !== null) { return ( ); @@ -100,7 +211,6 @@ export const WelcomeScreen: React.FC = ({ {!hasInitialRoomName && } - ); diff --git a/src/components/web3/EIP4361.test.ts b/src/components/web3/EIP4361.test.ts index 2db523a4..68f8d406 100644 --- a/src/components/web3/EIP4361.test.ts +++ b/src/components/web3/EIP4361.test.ts @@ -38,7 +38,7 @@ Resources: "https://example.com/my-web2-claim.json", ], }; - const actual = createEIP4361Message(message); + const actual = createEIP4361Message(message, "Ethereum"); expect(actual).toEqual(expected); }); diff --git a/src/components/web3/EIP4361.ts b/src/components/web3/EIP4361.ts index 23bdf857..b22d82e5 100644 --- a/src/components/web3/EIP4361.ts +++ b/src/components/web3/EIP4361.ts @@ -15,8 +15,11 @@ export interface EIP4361Message { resources?: string[]; } -export const createEIP4361Message = (message: EIP4361Message): string => { - let result = `${message.domain} wants you to sign in with your Ethereum account:\n${message.address}\n\n`; +export const createEIP4361Message = ( + message: EIP4361Message, + account: string +): string => { + let result = `${message.domain} wants you to sign in with your ${account} account:\n${message.address}\n\n`; if (message.statement) { result = result.concat(message.statement, "\n"); diff --git a/src/components/web3/JoinCall.tsx b/src/components/web3/JoinCall.tsx index 94d4d1cb..90544c3d 100644 --- a/src/components/web3/JoinCall.tsx +++ b/src/components/web3/JoinCall.tsx @@ -4,6 +4,7 @@ import { web3NFTs } from "./api"; import { rememberAvatarUrl, NFT } from "./core"; import { JitsiContext } from "../../jitsi/types"; import { Login } from "./Login"; +import { SolLogin } from "./SolLogin"; import { OptionalSettings } from "./OptionalSettings"; import { bodyText, header } from "./styles"; import { useWeb3CallState } from "../../hooks/use-web3-call-state"; @@ -16,12 +17,16 @@ interface Props { setJwt: (jwt: string) => void; jitsiContext: JitsiContext; setJitsiContext: (context: JitsiContext) => void; + web3Account: "ETH" | "SOL" | null; + setWeb3Account: (web3Account: "ETH" | "SOL") => void; } export const JoinCall: React.FC = ({ roomName, setJwt, jitsiContext, + web3Account, + setWeb3Account, setJitsiContext, }) => { const { t } = useTranslation(); @@ -43,7 +48,7 @@ export const JoinCall: React.FC = ({ setParticipantNFTCollections, setModeratorNFTCollections, joinCall, - } = useWeb3CallState(setFeedbackMessage); + } = useWeb3CallState(setFeedbackMessage, web3Account, setWeb3Account); // this magic says "run this function when the web3address changes" useEffect(() => { @@ -94,12 +99,20 @@ export const JoinCall: React.FC = ({ >
Join a Web3 Call
- + {web3Account === "ETH" ? ( + + ) : ( + + )} {web3Address && (
void; permissionType: string; setPermissionType: (permissionType: Web3PermissionType) => void; - participantPoaps: POAP[]; - setParticipantPoaps: (participantPoaps: POAP[]) => void; - moderatorPoaps: POAP[]; - setModeratorPoaps: (moderatorPoaps: POAP[]) => void; + participantPoaps?: POAP[]; + setParticipantPoaps?: (participantPoaps: POAP[]) => void; + moderatorPoaps?: POAP[]; + setModeratorPoaps?: (moderatorPoaps: POAP[]) => void; participantNFTCollections: NFTcollection[]; setParticipantNFTCollections: ( participantNFTCollections: NFTcollection[] @@ -36,6 +37,7 @@ interface Props { export const OptionalSettings: React.FC = ({ startCall, + web3Account, nfts = [], poaps, nftCollections, @@ -43,10 +45,14 @@ export const OptionalSettings: React.FC = ({ setNft, permissionType, setPermissionType, - participantPoaps, - setParticipantPoaps, - moderatorPoaps, - setModeratorPoaps, + participantPoaps = [], + setParticipantPoaps = () => { + return []; + }, + moderatorPoaps = [], + setModeratorPoaps = () => { + return []; + }, participantNFTCollections, setParticipantNFTCollections, moderatorNFTCollections, @@ -96,6 +102,7 @@ export const OptionalSettings: React.FC = ({ )} diff --git a/src/components/web3/PermissionTypeSelector.tsx b/src/components/web3/PermissionTypeSelector.tsx index 492ae950..fe90fff2 100644 --- a/src/components/web3/PermissionTypeSelector.tsx +++ b/src/components/web3/PermissionTypeSelector.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { Web3PermissionType } from "./api"; type Props = { + web3Account: "ETH" | "SOL" | null; permissionType: string; setPermissionType: (permissionType: Web3PermissionType) => void; }; @@ -31,17 +32,26 @@ const styles = { "&:hover": { background: "rgba(255, 255, 255, 0.42)" }, }), }; +const disabledButton = css({ + pointerEvents: "none", + opacity: 0.5, +}); export const PermissionTypeSelector: React.FC = ({ setPermissionType, permissionType, + web3Account, }) => { const { t } = useTranslation(); return (
@@ -56,7 +66,8 @@ export const PermissionTypeSelector: React.FC = ({ diff --git a/src/components/web3/SolLogin.tsx b/src/components/web3/SolLogin.tsx new file mode 100644 index 00000000..4069dcb6 --- /dev/null +++ b/src/components/web3/SolLogin.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { bodyText, walletAddress } from "./styles"; +import { web3LoginSol } from "./api"; + +interface Props { + web3address?: string; + onAddressSelected: (address: string, event: string) => void; +} + +export const SolLogin: React.FC = ({ + web3address, + onAddressSelected, +}) => { + const [notice, setNotice] = useState(); + const { t } = useTranslation(); + + useEffect(() => { + try { + window.braveSolana?.on("!!! accountChanged", () => setNotice(undefined)); + } catch { + console.warn("!!! Brave Wallet does not exist"); + } + + try { + window.phantom?.solana.on("!!! accountChanged", () => + setNotice(undefined) + ); + } catch { + console.warn("!!! Phantom Wallet does not exist"); + } + }, []); + + useEffect(() => { + if (!web3address) { + setNotice({t("wallet_connect_request")}); + web3LoginSol() + .then((address) => { + onAddressSelected(address, "login"); + setNotice(undefined); + }) + .catch((err) => { + console.error("!!! web3 window.braveSolana.connect", err); + setNotice( + +
+

+ Failed to connect to wallet. Please reload the page and try + again. +

+ + Help with Brave Wallet + +
+
+ ); + }); + } else { + setNotice(undefined); + } + }, [web3address, onAddressSelected, t]); + + return ( + <> + {notice &&
{notice}
} + + {web3address && ( + <> +
{t("wallet_address")}
+
+ {web3address} +
+ + )} + + ); +}; diff --git a/src/components/web3/StartCall.tsx b/src/components/web3/StartCall.tsx index 8196fc81..80a5fd57 100644 --- a/src/components/web3/StartCall.tsx +++ b/src/components/web3/StartCall.tsx @@ -5,6 +5,7 @@ import { JitsiContext } from "../../jitsi/types"; import { web3NFTs, web3POAPs, web3NFTcollections } from "./api"; import { POAP, NFT, NFTcollection, rememberAvatarUrl } from "./core"; import { Login } from "./Login"; +import { SolLogin } from "./SolLogin"; import { OptionalSettings } from "./OptionalSettings"; import { bodyText, header } from "./styles"; import { useWeb3CallState } from "../../hooks/use-web3-call-state"; @@ -17,6 +18,8 @@ type Props = { setRoomName: (roomName: string) => void; jitsiContext: JitsiContext; setJitsiContext: (context: JitsiContext) => void; + web3Account: "ETH" | "SOL" | null; + setWeb3Account: (web3Account: "ETH" | "SOL") => void; isSubscribed: boolean; }; @@ -25,6 +28,8 @@ export const StartCall: React.FC = ({ setRoomName, jitsiContext, setJitsiContext, + web3Account, + setWeb3Account, isSubscribed, }) => { const { t } = useTranslation(); @@ -52,7 +57,7 @@ export const StartCall: React.FC = ({ setParticipantNFTCollections, setModeratorNFTCollections, startCall, - } = useWeb3CallState(setFeedbackMessage); + } = useWeb3CallState(setFeedbackMessage, web3Account, setWeb3Account); // this magic says "run this function when the web3address changes" useEffect(() => { @@ -66,14 +71,14 @@ export const StartCall: React.FC = ({ console.error("!!! failed to fetch NFTs ", err); setFeedbackMessage("identifier_fetch_error"); }); - - web3POAPs(web3Address) - .then(setPoaps) - .catch((err) => { - console.error("!!! failed to fetch POAPs ", err); - setFeedbackMessage("identifier_fetch_error"); - }); - + if (web3Account === "ETH") { + web3POAPs(web3Address) + .then(setPoaps) + .catch((err) => { + console.error("!!! failed to fetch POAPs ", err); + setFeedbackMessage("identifier_fetch_error"); + }); + } web3NFTcollections(web3Address) .then(setNFTCollections) .catch((err) => { @@ -81,7 +86,7 @@ export const StartCall: React.FC = ({ setFeedbackMessage("identifier_fetch_error"); }); } - }, [web3Address]); + }, [web3Address, web3Account]); const onChangeDebugMode = () => { setDebugMode(!debugMode); @@ -121,37 +126,63 @@ export const StartCall: React.FC = ({ >
Start a Web3 Call
{isNFTDebug && ( - + + )} + + {web3Account === "ETH" ? ( + + ) : ( + )} - {web3Address && (!isNFTDebug || !debugMode) && (
- - + {web3Account === "ETH" ? ( + + ) : ( + + )}
{feedbackMessage ? t(feedbackMessage) : ""}
diff --git a/src/components/web3/api.ts b/src/components/web3/api.ts index f2a084a1..a3d41378 100644 --- a/src/components/web3/api.ts +++ b/src/components/web3/api.ts @@ -2,6 +2,7 @@ import { isProduction } from "../../environment"; import { fetchWithTimeout } from "../../lib"; import { NFTcollection, POAP, NFT } from "./core"; import { EIP4361Message, createEIP4361Message } from "./EIP4361"; +import { Buffer } from "buffer"; declare let window: any; @@ -38,6 +39,11 @@ export interface Web3BalancesRequireList { minimum: string; // in wei, e.g. 10e-18 } +export interface Web3SolAuthorization { + method: string; + Collections: Web3AuthList; +} + export interface Web3AuthList { participantADs: Web3ResourceIdentifierList; moderatorADs: Web3ResourceIdentifierList; @@ -54,6 +60,12 @@ export interface Web3RequestBody { avatarURL: string | null; } +export interface Web3SolRequestBody { + web3Authentication: Web3Authentication; + web3Authorization?: Web3SolAuthorization; + avatarURL: string | null; +} + export const web3Login = async (): Promise => { const allAddresses: string[] = await window.ethereum.request({ method: "eth_requestAccounts", @@ -64,9 +76,21 @@ export const web3Login = async (): Promise => { return allAddresses[0]; }; +export const web3LoginSol = async (): Promise => { + try { + const result = await window.braveSolana.connect(); + console.log("!!! allAddresses", result); + return result.publicKey.toBase58(); + } catch { + const result = await window.phantom.solana.connect(); + console.log("!!! allAddresses", result); + return result.publicKey.toBase58(); + } +}; + export const web3NFTs = async (address: string): Promise => { try { - const getNFTsURL = `${SIMPLEHASH_PROXY_ROOT_URL}/api/v0/nfts/owners?chains=ethereum,polygon&wallet_addresses=${encodeURIComponent( + const getNFTsURL = `${SIMPLEHASH_PROXY_ROOT_URL}/api/v0/nfts/owners?chains=ethereum,solana,polygon&wallet_addresses=${encodeURIComponent( address )}`; console.log(`>>> GET ${getNFTsURL}`); @@ -162,7 +186,7 @@ export const web3NFTcollections = async ( }; try { - const getNFTsByWalletURL = `${SIMPLEHASH_PROXY_ROOT_URL}/api/v0/nfts/owners?chains=ethereum,polygon&wallet_addresses=${encodeURIComponent( + const getNFTsByWalletURL = `${SIMPLEHASH_PROXY_ROOT_URL}/api/v0/nfts/owners?chains=ethereum,solana,polygon&wallet_addresses=${encodeURIComponent( address )}`; console.log(`>>> GET ${getNFTsByWalletURL}`); @@ -214,7 +238,7 @@ export const web3Prove = async ( nonce: nonce, issuedAt: new Date().toISOString(), }; - const payload = createEIP4361Message(message); + const payload = createEIP4361Message(message, "Ethereum"); const payloadBytes = new TextEncoder().encode(payload); const { hexlify } = await import("ethers"); const hexPayload = hexlify(payloadBytes); @@ -234,6 +258,62 @@ export const web3Prove = async ( return result; }; +export const web3SolProve = async ( + web3Address: string +): Promise => { + if (!web3Address) { + throw new Error("not logged into Web3"); + } + + const nonce = await getNonce(); + console.log("!!! nonce", nonce); + const message: EIP4361Message = { + domain: window.location.host, + address: web3Address, + statement: + "Please sign this message so Brave Talk knows that you own this address", + uri: window.location.toString(), + version: "1", + chainId: 1, + // HT:https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex/40031979 + // btoa has some characters not allowed by the EIP-4361 ABNF + nonce: nonce, + issuedAt: new Date().toISOString(), + }; + const payload = createEIP4361Message(message, "Solana"); + const payloadBytes = new TextEncoder().encode(payload); + const { hexlify } = await import("ethers"); + const hexPayload = hexlify(payloadBytes); + try { + const { publicKey, signature } = await window.braveSolana.signMessage( + payloadBytes + ); + const result = { + method: "CAIP-122-json", + proof: { + signer: publicKey.toBase58(), + signature: Buffer.from(signature).toString("hex"), + payload: hexPayload, + }, + }; + return result; + } catch { + const { publicKey, signature } = await window.phantom.solana.signMessage( + payloadBytes + ); + const result = { + method: "CAIP-122-json", + proof: { + signer: publicKey.toBase58(), + signature: Buffer.from(signature).toString("hex"), + payload: hexPayload, + }, + }; + + return result; + } +}; + const poapContractAddress = "0x22c1f6050e56d2876009903609a2cc3fef83b415"; const poapContractChain = "gnosis"; diff --git a/src/hooks/use-call-setup-status.ts b/src/hooks/use-call-setup-status.ts index 603b598d..cd0e8dc2 100644 --- a/src/hooks/use-call-setup-status.ts +++ b/src/hooks/use-call-setup-status.ts @@ -34,6 +34,7 @@ interface JoinConferenceRoomResult { jwt?: string; retryLater?: boolean; retryAsWeb3?: boolean; + web3account?: string; } const fetchOrCreateJWT = async ( @@ -66,6 +67,10 @@ const fetchOrCreateJWT = async ( return await fetchOrCreateJWT(roomName, true, false, notice); } else if (error.message.includes("Retry as Web3 call")) { return { retryAsWeb3: true }; + } else if (error.message.includes("ETH")) { + return { web3account: "ETH" }; + } else if (error.message.includes("SOL")) { + return { web3account: "SOL" }; } else { console.error(error); notice(error.message); @@ -85,6 +90,8 @@ interface CallSetup { isCallReady: boolean; isWeb3Call: boolean; setIsWeb3Call: (isWeb3Call: boolean) => void; + web3Account: "ETH" | "SOL" | null; + setWeb3Account: (web3Account: "ETH" | "SOL" | null) => void; setJwt: (jwt: string) => void; setRoomName: (roomName: string) => void; setJitsiContext: (jitsiContext: JitsiContext) => void; @@ -102,6 +109,7 @@ export function useCallSetupStatus( // on it const [hasInitialRoom, setHasInitialRoom] = useState(() => !!roomName); const [isWeb3Call, setIsWeb3Call] = useState(false); + const [web3Account, setWeb3Account] = useState<"ETH" | "SOL" | null>(null); const [jwt, setJwt] = useState(); const [notice, setNotice] = useState(); const [isEstablishingCall, setIsEstablishingCall] = useState(false); @@ -148,9 +156,12 @@ export function useCallSetupStatus( setJwt(result.jwt); } - if (result.retryAsWeb3) { - // Convert this to a web3 call + if (result.web3account === "ETH") { setIsWeb3Call(true); + setWeb3Account("ETH"); + } else if (result.web3account === "SOL") { + setIsWeb3Call(true); + setWeb3Account("SOL"); } else { // the error message has already been displayed by fetchOrCreateJWT, // but we need to allow the user to recover by enabling all functionality @@ -192,6 +203,8 @@ export function useCallSetupStatus( isCallReady, isWeb3Call, setIsWeb3Call, + web3Account, + setWeb3Account, setJwt, setRoomName, setJitsiContext, diff --git a/src/hooks/use-params.ts b/src/hooks/use-params.ts index 6faed25e..87c8b81e 100644 --- a/src/hooks/use-params.ts +++ b/src/hooks/use-params.ts @@ -9,6 +9,8 @@ interface Params { // rather than entering the room. This is used by the google calendar extension. isCreateOnly: boolean; + // url has "sol=y" meaning we should show option to start either solana or ethereum call + isSolana: boolean; isDebug: boolean; } @@ -20,6 +22,7 @@ export function useParams(): Params { return { isCreate: p.get("create") === "y", isCreateOnly: p.get("create_only") === "y", + isSolana: p.get("sol") === "y", isDebug: p.get("debug") === "y", }; }); diff --git a/src/hooks/use-web3-call-state.ts b/src/hooks/use-web3-call-state.ts index e4d29b0d..513fb098 100644 --- a/src/hooks/use-web3-call-state.ts +++ b/src/hooks/use-web3-call-state.ts @@ -4,6 +4,7 @@ import { Web3RequestBody, Web3Authentication, web3Prove, + web3SolProve, Web3PermissionType, } from "../components/web3/api"; import { POAP, NFTcollection, NFT } from "../components/web3/core"; @@ -36,7 +37,9 @@ interface Web3CallState { } export function useWeb3CallState( - setFeedbackMessage: (message: TranslationKeys) => void + setFeedbackMessage: (message: TranslationKeys) => void, + web3Account: "ETH" | "SOL" | null, + setWeb3Account: (web3Account: "ETH" | "SOL") => void ): Web3CallState { const [web3Address, _setWeb3Address] = useState(); const [permissionType, setPermissionType] = @@ -61,6 +64,9 @@ export function useWeb3CallState( case "accountsChanged": { return address; } + case "accountChanged": { + return address; + } default: { return address; } @@ -68,10 +74,44 @@ export function useWeb3CallState( }); }; - window.ethereum?.on("accountsChanged", (accounts: string[]) => { - console.log("!!! accountsChanged", accounts); - setWeb3Address(accounts[0], "accountsChanged"); - }); + if (web3Account === "ETH") { + window.ethereum?.on("accountsChanged", (accounts: string[]) => { + console.log("!!! ETH accountsChanged", accounts); + setWeb3Account("ETH"); + setWeb3Address(accounts[0], "accountsChanged"); + }); + } + + try { + window.braveSolana?.on("accountChanged", (account: any) => { + setWeb3Account("SOL"); + if (account) { + console.log("!!! SOL accountChanged", account.toBase58()); + setWeb3Address(account.toBase58(), "accountsChanged"); + } else { + console.log("!!! SOL accountChanged", account); + setWeb3Address(account, "accountsChanged"); + } + }); + } catch { + console.warn("!!! Brave Wallet does not exists"); + } + + try { + window.phantom?.solana.on("accountChanged", (account: any) => { + setWeb3Account("SOL"); + console.log(account); + if (account) { + console.log("!!! SOL accountChanged", account.toBase58()); + setWeb3Address(account.toBase58(), "accountsChanged"); + } else { + console.log("!!! SOL accountChanged", account); + setWeb3Address(account, "accountsChanged"); + } + }); + } catch { + console.warn("!!! Phantom Wallet does not exists"); + } const joinCall = async ( roomName: string @@ -80,7 +120,11 @@ export function useWeb3CallState( let auth: Web3Authentication | null = null; let jwt = ""; try { - auth = await web3Prove(web3Address as string); + if (web3Account === "ETH") { + auth = await web3Prove(web3Address as string); + } else { + auth = await web3SolProve(web3Address as string); + } web3 = { web3Authentication: auth, avatarURL: nft != null ? nft.image_url : "", @@ -125,12 +169,18 @@ export function useWeb3CallState( [string, string, Web3Authentication] | undefined > => { try { - const auth = await web3Prove(web3Address as string); + let auth: Web3Authentication | null = null; + if (web3Account === "ETH") { + auth = await web3Prove(web3Address as string); + } else { + auth = await web3SolProve(web3Address as string); + } const roomName = generateRoomName(); const web3 = { web3Authentication: auth, web3Authorization: { method: permissionType, + account: web3Account, POAPs: { participantADs: { allow: participantPoaps.map((p) => p.event.id.toString()), diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 726baacf..918695bf 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -1,6 +1,6 @@ { "avatar_fetch_error": "Failed to fetch avatar NFTs", - "avatar_nft_subhead": "Currently supports Ethereum ERC-721 NFTs and ERC-1155 NFTs ", + "avatar_nft_subhead": "Currently supports Ethereum ERC-721 NFTs, ERC-1155 NFTs and Solana Metaplex NFTs ", "bat_gating_panel_header": "Non-Zero BAT gating will be applied.", "bat_gating_panel_subheader": "Users will need to have a non-zero balance of BAT in their wallet to join this call.", "call_permission_type": "Call permission type:", @@ -58,6 +58,7 @@ "your_avatar_nft_header": "Your Avatar NFT", "premium_card_title": "Premium calls", "web3_card_title": "Host a Web3 call", + "web3_account_body": "Choose the network of the account you want to connect start the Web3 call.", "Bad gateway": "Bad gateway", "Checking moderator status...": "Checking moderator status...", diff --git a/src/i18n/locales/jp/translation.json b/src/i18n/locales/jp/translation.json index 9cec5786..811b3947 100644 --- a/src/i18n/locales/jp/translation.json +++ b/src/i18n/locales/jp/translation.json @@ -1,6 +1,6 @@ { "avatar_fetch_error": "アバターNFTの取得に失敗しました", - "avatar_nft_subhead": "現在イーサリアムERC-721 NFTをサポートしています", + "avatar_nft_subhead": "Ethereum ERC-721のNFT、Solana MetaplexのNFTが使用可能です。", "bat_gating_panel_header": "BATを用いたトークンゲート(入場制限)が適用されます。", "bat_gating_panel_subheader": "ウォレットでBATを保有していない場合(0BATの場合)、このコールには参加できません。", "call_permission_type": "通話の許可タイプ:", @@ -58,6 +58,7 @@ "your_avatar_nft_header": "あなたのアバターNFT", "premium_card_title": "プレミアム通話", "web3_card_title": "Web3通話を開始する", + "web3_account_body": "Web3コールを開始するにあたって使用するアカウントのネットワークを選択してください。", "Bad gateway": "バッド・ゲートウェイ", "Checking moderator status...": "モデレーターの状態を確認しています...", diff --git a/src/images/ethereum.svg b/src/images/ethereum.svg new file mode 100644 index 00000000..8520eb4d --- /dev/null +++ b/src/images/ethereum.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/images/solana.svg b/src/images/solana.svg new file mode 100644 index 00000000..55314d1d --- /dev/null +++ b/src/images/solana.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/jitsi/options.ts b/src/jitsi/options.ts index e3fbb903..fff7f993 100644 --- a/src/jitsi/options.ts +++ b/src/jitsi/options.ts @@ -182,6 +182,7 @@ export const jitsiOptions = ( }; const features = jwt_decode(jwt)?.context?.features; + reportAction("features", { features }); Object.entries(features).forEach(([feature, state]) => { diff --git a/src/rooms.ts b/src/rooms.ts index 613c4fef..087a3776 100644 --- a/src/rooms.ts +++ b/src/rooms.ts @@ -98,6 +98,11 @@ const roomsRequest = async ({ }); } const respText = await response.text(); + if (respText.includes("ETH")) { + throw new Error("ETH"); + } else if (respText.includes("SOL")) { + throw new Error("SOL"); + } const message = status == 401 ? respText @@ -245,7 +250,6 @@ export const fetchJWT = async ( }); store.storeJwtForRoom(roomName, response.jwt, response.refresh); - return { jwt: response.jwt, url: "//" + window.location.host + "/" + roomName, diff --git a/src/types.d.ts b/src/types.d.ts index d96b9b8e..1391ce71 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -7,6 +7,8 @@ declare let JitsiMeetExternalAPI: any; declare function jwt_decode(input: string): any; interface Window { + braveSolana: any; + phantom: any; ethereum: any; chrome?: { braveRequestAdsEnabled?: () => Promise;