From 0a8ee99da309a64794c927ead9482683cea4ea83 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Tue, 2 Jul 2024 15:37:46 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A6=8A=20Metamask=20integration=20POC?= =?UTF-8?q?=20(#4835)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Temporarily generate the http-rpc endpoint from the ws one * Add Metamask wallet * Use `@chainsafe/polkadot-snap` * Set RPC node HTTP endpoint * Fix failing test * Fix metamask detection * Remove unnecessary code * Do not auto select wallet extensions --- packages/ui/.env.example | 5 + packages/ui/package.json | 2 + packages/ui/src/accounts/model/metamask.ts | 83 ++++++++++++++++ .../accounts/providers/accounts/useWallets.ts | 55 ++++++++--- .../src/app/assets/images/logos/Metamask.svg | 43 +++++++++ packages/ui/src/app/config/network.ts | 11 +++ .../app/pages/Settings/SettingsNetworkTab.tsx | 96 ++++++++++++++----- .../components/NetworkInfo/NetworkInfo.tsx | 32 +++++-- packages/ui/src/common/types/index.ts | 2 + .../CurrentMember/CurrentMember.tsx | 16 +--- .../src/services/i18n/dict/en/settings.json | 11 ++- packages/ui/test/app/pages/Settings.test.tsx | 6 +- yarn.lock | 67 ++++++++++++- 13 files changed, 366 insertions(+), 63 deletions(-) create mode 100644 packages/ui/src/accounts/model/metamask.ts create mode 100644 packages/ui/src/app/assets/images/logos/Metamask.svg diff --git a/packages/ui/.env.example b/packages/ui/.env.example index 209afd5b88..dc473fbda6 100644 --- a/packages/ui/.env.example +++ b/packages/ui/.env.example @@ -1,5 +1,6 @@ # TESTNET Endpoints REACT_APP_TESTNET_NODE_SOCKET=wss://rpc.joystream.org:9944 +REACT_APP_TESTNET_NODE_HTTP_RPC=https://rpc.joystream.org REACT_APP_TESTNET_QUERY_NODE=https://query.joystream.org/graphql REACT_APP_TESTNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql REACT_APP_TESTNET_MEMBERSHIP_FAUCET_URL=https://faucet.joystream.org/member-faucet/register @@ -7,6 +8,7 @@ REACT_APP_TESTNET_BACKEND=http://localhost:3000 # MAINNET Endpoints REACT_APP_MAINNET_NODE_SOCKET=wss://rpc.joystream.org:9944 +REACT_APP_MAINNET_NODE_HTTP_RPC=https://rpc.joystream.org REACT_APP_MAINNET_QUERY_NODE=https://query.joystream.org/graphql REACT_APP_MAINNET_QUERY_NODE_SOCKET=wss://query.joystream.org/graphql REACT_APP_MAINNET_MEMBERSHIP_FAUCET_URL=https://faucet.joystream.org/member-faucet/register @@ -23,6 +25,9 @@ REACT_APP_AVATAR_UPLOAD_URL=https://atlas-services.joystream.org/avatars # WalletConnect project id REACT_APP_WALLET_CONNECT_PROJECT_ID="2ea3f3ghubh32b8ie2f2" +# Metamask snap id (`local:http://localhost:8081` for local development) +REACT_APP_METAMASK_SNAP_ID="npm:@chainsafe/polkadot-snap" + # Image reporting ## Manual blacklist: diff --git a/packages/ui/package.json b/packages/ui/package.json index fa9c2700fd..3e43219cba 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@apollo/client": "3.5.7", + "@chainsafe/metamask-polkadot-adapter": "^0.6.0", "@hcaptcha/react-hcaptcha": "^1.4.4", "@joystream/js": "1.10.0", "@joystream/markdown-editor": "^0.1.0", @@ -97,6 +98,7 @@ "@babel/preset-env": "7", "@babel/preset-react": "7", "@babel/preset-typescript": "7", + "@chainsafe/metamask-polkadot-types": "^0.6.0", "@graphql-codegen/cli": "^2.2.0", "@graphql-codegen/near-operation-file-preset": "^2.1.4", "@graphql-codegen/typescript": "^2.2.2", diff --git a/packages/ui/src/accounts/model/metamask.ts b/packages/ui/src/accounts/model/metamask.ts new file mode 100644 index 0000000000..e6542720f1 --- /dev/null +++ b/packages/ui/src/accounts/model/metamask.ts @@ -0,0 +1,83 @@ +import { enablePolkadotSnap } from '@chainsafe/metamask-polkadot-adapter' +import { MetamaskSnapApi } from '@chainsafe/metamask-polkadot-adapter/build/types' +import { SnapNetworks } from '@chainsafe/metamask-polkadot-types' +import { Signer } from '@polkadot/types/types' +import { BaseDotsamaWallet, SubscriptionFn, WalletAccount } from 'injectweb3-connect' + +import MetamaskLogo from '@/app/assets/images/logos/Metamask.svg' +import { CHAIN_PROPERTIES } from '@/app/constants/chain' + +const networkName = 'joystream' as SnapNetworks +const addressPrefix = CHAIN_PROPERTIES.ss58Format +const unit = { symbol: CHAIN_PROPERTIES.tokenSymbol[0], decimals: CHAIN_PROPERTIES.tokenDecimals[0] } + +export class Metamask extends BaseDotsamaWallet { + protected _snapId: string + protected _httpRpcUrl: string + protected _snapApi: MetamaskSnapApi | undefined + protected _accounts: WalletAccount[] | undefined + protected _txId = 0 + + constructor(snapId: string, httpRpcUrl: string) { + super({ + extensionName: 'Metamask', + title: 'Metamask', + logo: { src: MetamaskLogo, alt: 'Metamask Logo' }, + }) + + this._snapId = snapId + this._httpRpcUrl = httpRpcUrl + } + + public enable = async (): Promise => { + const snap = await enablePolkadotSnap( + { networkName, wsRpcUrl: this._httpRpcUrl, addressPrefix, unit }, + this._snapId + ) + + this._snapApi = await snap.getMetamaskSnapApi() + const address = await this._snapApi.getAddress() + this._accounts = [ + { + name: 'Metamask account', + address, + source: this.extensionName, + }, + ] + + this._snapApi.signPayloadJSON + } + + public getAccounts = async (): Promise => { + return this._accounts ?? [] + } + + public subscribeAccounts: (callback: SubscriptionFn) => Promise<() => void> = (callback) => { + callback(this._accounts ?? []) + return Promise.resolve(() => undefined) + } + + public get signer(): Signer { + return { + signPayload: async (payload) => { + if (!this._snapApi) { + throw Error('Metamask was accessed before it was enabled') + } + + const signature = (await this._snapApi.signPayloadJSON(payload)) as `0x${string}` + + return { id: this._txId++, signature } + }, + + signRaw: async (raw) => { + if (!this._snapApi) { + throw Error('Metamask was accessed before it was enabled') + } + + const signature = (await this._snapApi.signPayloadRaw(raw)) as `0x${string}` + + return { id: this._txId++, signature } + }, + } + } +} diff --git a/packages/ui/src/accounts/providers/accounts/useWallets.ts b/packages/ui/src/accounts/providers/accounts/useWallets.ts index 9c5ac3de6e..a66ac4fc21 100644 --- a/packages/ui/src/accounts/providers/accounts/useWallets.ts +++ b/packages/ui/src/accounts/providers/accounts/useWallets.ts @@ -3,10 +3,12 @@ import { groupBy } from 'lodash' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Subject, firstValueFrom } from 'rxjs' +import { Metamask } from '@/accounts/model/metamask' import { WalletConnect } from '@/accounts/model/walletConnect' import { RecommendedWallets, RecommendedWalletsNames, asWallet } from '@/accounts/model/wallets' import { useApi } from '@/api/hooks/useApi' import { useLocalStorage } from '@/common/hooks/useLocalStorage' +import { useNetworkEndpoints } from '@/common/hooks/useNetworkEndpoints' type WalletState = undefined | 'ENABLING' | 'READY' | 'APP_REJECTED' @@ -21,6 +23,27 @@ const genesisHash$ = new Subject() const WalletDisconnection$ = new Subject() export const useWallets = (): UseWallets => { + const walletExtensions = useWalletExtensions() + const walletConnect = useWalletConnect(() => setWallet(undefined)) + const metamask = useMetamask() + + const allWallets: Wallet[] = useMemo( + () => [ + ...walletExtensions.installed, + ...walletExtensions.unknown, + ...metamask, + ...walletConnect, + ...walletExtensions.recommended, + ], + [walletExtensions, walletConnect] + ) + + const { wallet, setWallet, walletState } = useSelectedWallet(allWallets) + + return { allWallets, wallet, setWallet, walletState } +} + +const useWalletExtensions = (): { installed: Wallet[]; recommended: Wallet[]; unknown: Wallet[] } => { const [installedWalletsNames, setInstalledWalletsNames] = useState([]) useEffect(() => { @@ -43,40 +66,44 @@ export const useWallets = (): UseWallets => { return () => clearInterval(intervalId) }, []) - const walletExtensions = useMemo(() => { + return useMemo(() => { const unknown = installedWalletsNames.filter((name) => !RecommendedWalletsNames.includes(name)).map(asWallet) const { installed = [], recommended = [] } = groupBy(RecommendedWallets, (wallet) => installedWalletsNames.includes(wallet.extensionName) ? 'installed' : 'recommended' ) return { installed, recommended, unknown } }, [installedWalletsNames]) +} +const useWalletConnect = (disconnect: () => void): WalletConnect[] => { const { api } = useApi() useEffect(() => { if (api) genesisHash$.next(api.genesisHash.toHex()) }, [api?.isConnected]) - const walletConnect = useMemo(() => { + const walletConnect: WalletConnect | undefined = useMemo(() => { const wcProjectId: string | undefined = process.env.REACT_APP_WALLET_CONNECT_PROJECT_ID if (!wcProjectId) return const genesisHash = firstValueFrom(genesisHash$) - return new WalletConnect(wcProjectId, genesisHash, WalletDisconnection$, () => setWallet(undefined)) + return new WalletConnect(wcProjectId, genesisHash, WalletDisconnection$, disconnect) }, []) - const allWallets = useMemo( - () => [ - ...walletExtensions.installed, - ...walletExtensions.unknown, - ...(walletConnect ? [walletConnect] : []), - ...walletExtensions.recommended, - ], - [walletExtensions, walletConnect] - ) + return useMemo(() => (walletConnect ? [walletConnect] : []), [walletConnect]) +} - const { wallet, setWallet, walletState } = useSelectedWallet(allWallets) +const useMetamask = (): Metamask[] => { + const [endpoints] = useNetworkEndpoints() - return { allWallets, wallet, setWallet, walletState } + const metamask: Metamask | undefined = useMemo(() => { + const snapId = process.env.REACT_APP_METAMASK_SNAP_ID + const isMetaMask = !!window.ethereum && '_metamask' in window.ethereum + if (!snapId || !isMetaMask) return + + return new Metamask(snapId, endpoints.nodeHttpRpcEndpoint) + }, [endpoints.nodeHttpRpcEndpoint]) + + return useMemo(() => (metamask ? [metamask] : []), [metamask]) } const useSelectedWallet = (allWallets: Wallet[]) => { diff --git a/packages/ui/src/app/assets/images/logos/Metamask.svg b/packages/ui/src/app/assets/images/logos/Metamask.svg new file mode 100644 index 0000000000..b81767d25b --- /dev/null +++ b/packages/ui/src/app/assets/images/logos/Metamask.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/app/config/network.ts b/packages/ui/src/app/config/network.ts index 53c928ebaf..2ed74ca57e 100644 --- a/packages/ui/src/app/config/network.ts +++ b/packages/ui/src/app/config/network.ts @@ -2,6 +2,7 @@ export type NetworkType = 'mainnet' | 'local' | 'testnet' | 'auto-conf' | 'local export interface NetworkEndpoints { nodeRpcEndpoint: string + nodeHttpRpcEndpoint: string queryNodeEndpoint: string queryNodeEndpointSubscription: string membershipFaucetEndpoint: string @@ -10,6 +11,7 @@ export interface NetworkEndpoints { } const TESTNET_NODE_SOCKET = process.env.REACT_APP_TESTNET_NODE_SOCKET +const TESTNET_NODE_HTTP_RPC = process.env.REACT_APP_TESTNET_NODE_HTTP_RPC const TESTNET_QUERY_NODE = process.env.REACT_APP_TESTNET_QUERY_NODE const TESTNET_QUERY_NODE_SOCKET = process.env.REACT_APP_TESTNET_QUERY_NODE_SOCKET const TESTNET_MEMBERSHIP_FAUCET_URL = process.env.REACT_APP_TESTNET_MEMBERSHIP_FAUCET_URL @@ -19,6 +21,7 @@ export const IS_TESTNET_DEFINED = TESTNET_NODE_SOCKET && TESTNET_QUERY_NODE && TESTNET_QUERY_NODE_SOCKET && TESTNET_MEMBERSHIP_FAUCET_URL const MAINNET_NODE_SOCKET = process.env.REACT_APP_MAINNET_NODE_SOCKET +const MAINNET_NODE_HTTP_RPC = process.env.REACT_APP_MAINNET_NODE_HTTP_RPC const MAINNET_QUERY_NODE = process.env.REACT_APP_MAINNET_QUERY_NODE const MAINNET_QUERY_NODE_SOCKET = process.env.REACT_APP_MAINNET_QUERY_NODE_SOCKET const MAINNET_MEMBERSHIP_FAUCET_URL = process.env.REACT_APP_MAINNET_MEMBERSHIP_FAUCET_URL @@ -57,6 +60,13 @@ const NODE_RPC_ENDPOINT: PredefinedEndpoint = { 'local-mocks': 'ws://127.0.0.1:9944', } +const NODE_HTTP_RPC_ENDPOINT: PredefinedEndpoint = { + mainnet: MAINNET_NODE_HTTP_RPC, + local: 'http://127.0.0.1:9933', + testnet: TESTNET_NODE_HTTP_RPC, + 'local-mocks': 'http://127.0.0.1:9933', +} + const BACKEND_ENDPOINT: PredefinedEndpoint = { mainnet: MAINNET_BACKEND, local: 'http://localhost:3000', @@ -66,6 +76,7 @@ const BACKEND_ENDPOINT: PredefinedEndpoint = { export const pickEndpoints = (network: NetworkType): Partial => ({ nodeRpcEndpoint: NODE_RPC_ENDPOINT[network], + nodeHttpRpcEndpoint: NODE_HTTP_RPC_ENDPOINT[network], queryNodeEndpoint: QUERY_NODE_ENDPOINT[network], queryNodeEndpointSubscription: QUERY_NODE_ENDPOINT_SUBSCRIPTION[network], membershipFaucetEndpoint: MEMBERSHIP_FAUCET_ENDPOINT[network], diff --git a/packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx b/packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx index e5b56843f6..2ced0b97cb 100644 --- a/packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx +++ b/packages/ui/src/app/pages/Settings/SettingsNetworkTab.tsx @@ -1,3 +1,4 @@ +import { random } from 'lodash' import React, { useState, useEffect } from 'react' import { useForm, FormProvider } from 'react-hook-form' import { useTranslation } from 'react-i18next' @@ -35,15 +36,18 @@ export const SettingsNetworkTab = () => { const [endpoints, fetchNetworkEndpoints] = useNetworkEndpoints() const form = useForm() - const [customFaucetEndpoint, customRpcEndpoint, customQueryEndpoint, customBackendEndpoint] = form.watch([ - 'settings.customFaucetEndpoint', - 'settings.customRpcEndpoint', - 'settings.customQueryEndpoint', - 'settings.customBackendEndpoint', - ]) + const [customFaucetEndpoint, customWsRpcEndpoint, customHttpRpcEndpoint, customQueryEndpoint, customBackendEndpoint] = + form.watch([ + 'settings.customFaucetEndpoint', + 'settings.customWsRpcEndpoint', + 'settings.customHttpRpcEndpoint', + 'settings.customQueryEndpoint', + 'settings.customBackendEndpoint', + ]) const [, storeCustomEndpoints] = useLocalStorage('custom_endpoint') const [isValidFaucetEndpoint, setIsValidFaucetEndpoint] = useState(true) - const [isValidRpcEndpoint, setIsValidRpcEndpoint] = useState(true) + const [isValidWsRpcEndpoint, setIsValidWsRpcEndpoint] = useState(true) + const [isValidHttpRpcEndpoint, setIsValidHttpRpcEndpoint] = useState(true) const [isValidQueryEndpoint, setIsValidQueryEndpoint] = useState(true) const [isValidBackendEndpoint, setIsValidBackendEndpoint] = useState(true) const [customSaveStatus, setCustomSaveStatus] = useState<'Init' | 'Saving' | 'Done'>('Init') @@ -57,7 +61,8 @@ export const SettingsNetworkTab = () => { useEffect(() => { if (network === 'custom') { - form.setValue('settings.customRpcEndpoint', endpoints.nodeRpcEndpoint) + form.setValue('settings.customWsRpcEndpoint', endpoints.nodeRpcEndpoint) + form.setValue('settings.customHttpRpcEndpoint', endpoints.nodeHttpRpcEndpoint) form.setValue('settings.customQueryEndpoint', endpoints.queryNodeEndpoint) form.setValue('settings.customFaucetEndpoint', endpoints.membershipFaucetEndpoint ?? '') form.setValue('settings.customBackendEndpoint', endpoints.backendEndpoint ?? '') @@ -66,14 +71,16 @@ export const SettingsNetworkTab = () => { useEffect(() => { if ( - isValidRpcEndpoint && + isValidWsRpcEndpoint && + isValidHttpRpcEndpoint && isValidQueryEndpoint && isValidFaucetEndpoint && isValidBackendEndpoint && customSaveStatus === 'Done' ) { storeCustomEndpoints({ - nodeRpcEndpoint: customRpcEndpoint, + nodeRpcEndpoint: customWsRpcEndpoint, + nodeHttpRpcEndpoint: customHttpRpcEndpoint, queryNodeEndpoint: customQueryEndpoint, queryNodeEndpointSubscription: customQueryEndpoint.replace(/^http?/, 'ws'), membershipFaucetEndpoint: customFaucetEndpoint || undefined, @@ -82,11 +89,12 @@ export const SettingsNetworkTab = () => { }) window.location.reload() } - }, [isValidFaucetEndpoint, isValidRpcEndpoint, isValidQueryEndpoint, customSaveStatus]) + }, [isValidFaucetEndpoint, isValidWsRpcEndpoint, isValidQueryEndpoint, customSaveStatus]) const saveSettings = async () => { if ( - !isValidRPCUrl(customRpcEndpoint) || + !isValidWsUrl(customWsRpcEndpoint) || + !isValidHttpUrl(customHttpRpcEndpoint) || !isValidQNUrl(customQueryEndpoint) || !isValidFaucetUrl(customFaucetEndpoint) || !isValidBackendUrl(customBackendEndpoint) @@ -97,7 +105,8 @@ export const SettingsNetworkTab = () => { setCustomSaveStatus('Saving') await Promise.all([ - checkEndpoint(customRpcEndpoint, checkRpcEndpoint, setIsValidRpcEndpoint), + checkEndpoint(customWsRpcEndpoint, checkWsRpcEndpoint, setIsValidWsRpcEndpoint), + checkEndpoint(customHttpRpcEndpoint, checkHttpRpcEndpoint, setIsValidHttpRpcEndpoint), checkEndpoint(customQueryEndpoint, checkGQLEndpoint, setIsValidQueryEndpoint), checkEndpoint(customFaucetEndpoint, checkFaucetEndpoint, setIsValidFaucetEndpoint), checkEndpoint(customBackendEndpoint, checkGQLEndpoint, setIsValidBackendEndpoint), @@ -127,17 +136,36 @@ export const SettingsNetworkTab = () => { !isValidRPCUrl(customRpcEndpoint), 'This RPC endpoint must start with ws or wss'], + [() => !isValidWsUrl(customWsRpcEndpoint), 'This WS RPC endpoint must start with ws or wss'], [ - () => !isValidRpcEndpoint, + () => !isValidWsRpcEndpoint, 'Connection Error. Sometimes it fails due to network speed. Please try to check once more', ] )} > - + + + + !isValidHttpUrl(customHttpRpcEndpoint), 'This HTTP RPC endpoint must start with http or https'], + [() => !isValidHttpRpcEndpoint, 'Connection Error'] + )} + > + { )} @@ -226,7 +256,8 @@ type IsValidOptions = { prefix?: 'https?' | 'wss?'; isRequired?: boolean } const isValid = (url: string, { prefix = 'https?', isRequired = true }: IsValidOptions = {}) => (isRequired === false && url === '') || RegExp(String.raw`${prefix}://\w+/?`, 'i').test(url) -const isValidRPCUrl = (url: string) => isValid(url, { prefix: 'wss?' }) +const isValidWsUrl = (url: string) => isValid(url, { prefix: 'wss?' }) +const isValidHttpUrl = (url: string) => isValid(url, { prefix: 'https?' }) const isValidQNUrl = (url: string) => isValid(url) const isValidFaucetUrl = (url: string) => isValid(url, { isRequired: false }) const isValidBackendUrl = (url: string) => isValid(url, { isRequired: false }) @@ -241,8 +272,8 @@ const checkEndpoint = async ( return isValid } -const checkRpcEndpoint = async (endpoint: string) => { - // check RPC endpoint +const checkWsRpcEndpoint = async (endpoint: string) => { + // check WS RPC endpoint try { return await new Promise((resolve) => { const ws = new WebSocket(endpoint) @@ -261,6 +292,25 @@ const checkRpcEndpoint = async (endpoint: string) => { } } +const checkHttpRpcEndpoint = async (endpoint: string) => { + // check HTTP RPC endpoint + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: random(Number.MAX_SAFE_INTEGER), + jsonrpc: '2.0', + method: 'system_name', + params: [], + }), + }) + return response.status < 400 && (await response.json()).result === 'Joystream Node' + } catch { + return false + } +} + const checkGQLEndpoint = async (endpoint: string) => { // check GraphQL endpoint try { diff --git a/packages/ui/src/common/components/NetworkInfo/NetworkInfo.tsx b/packages/ui/src/common/components/NetworkInfo/NetworkInfo.tsx index 9730b6139b..80b67505d6 100644 --- a/packages/ui/src/common/components/NetworkInfo/NetworkInfo.tsx +++ b/packages/ui/src/common/components/NetworkInfo/NetworkInfo.tsx @@ -10,24 +10,42 @@ import { TextMedium } from '@/common/components/typography' export interface NetworkInfoProps { detailsTitle: string - networkAddress: string - queryNodeAddress: string + networkWsAddress: string + networkHttpAddress: string + queryNodeHttpAddress: string + queryNodeWsAddress: string faucetAddress?: string backendAddress?: string } const NetworkInfo: React.FC = React.memo( - ({ detailsTitle, networkAddress, queryNodeAddress, faucetAddress, backendAddress }) => { + ({ + detailsTitle, + networkWsAddress, + networkHttpAddress, + queryNodeHttpAddress, + queryNodeWsAddress, + faucetAddress, + backendAddress, + }) => { const { t } = useTranslation('settings') return ( }> - {t('networkAddress')} - + {t('networkSubscriptionAddress')} + - {t('QueryNodeAddress')} - + {t('networkHTTPAddress')} + + + + {t('QueryNodeHttpAddress')} + + + + {t('QueryNodeSubscriptionAddress')} + {t('faucet')} diff --git a/packages/ui/src/common/types/index.ts b/packages/ui/src/common/types/index.ts index 6922688ef2..4f0737242c 100644 --- a/packages/ui/src/common/types/index.ts +++ b/packages/ui/src/common/types/index.ts @@ -9,3 +9,5 @@ export type AnyKeys = { } export type AnyObject = Record + +export type Optional = Omit & Partial> diff --git a/packages/ui/src/memberships/components/CurrentMember/CurrentMember.tsx b/packages/ui/src/memberships/components/CurrentMember/CurrentMember.tsx index 9ace9e2e38..d610e75cf7 100644 --- a/packages/ui/src/memberships/components/CurrentMember/CurrentMember.tsx +++ b/packages/ui/src/memberships/components/CurrentMember/CurrentMember.tsx @@ -2,7 +2,6 @@ import React, { FC, useEffect } from 'react' import { useHistory } from 'react-router' import styled from 'styled-components' -import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { ButtonPrimary } from '@/common/components/buttons' import { ArrowDownExpandedIcon, Icon, Loader } from '@/common/components/icons' import { BorderRad, Colors, Transitions } from '@/common/constants' @@ -19,7 +18,6 @@ import { EmailSubscriptionModalCall } from '../../modals/EmailSubscriptionModal' import { SwitchMemberModalCall } from '../../modals/SwitchMemberModal' export const CurrentMember = () => { - const { allWallets, setWallet } = useMyAccounts() const { status, isLoading } = useOnBoarding() const { members, hasMembers, active } = useMyMemberships() const { showModal, modal } = useModal() @@ -57,18 +55,14 @@ export const CurrentMember = () => { } }, [emailVerificationToken]) - const handleOnboarding = () => { - const wallets = allWallets.filter((wallet) => wallet.installed) - if (wallets.length === 1) { - setWallet?.(wallets.at(-1)) - } - showModal({ modal: 'OnBoardingModal' }) - } - if (status !== 'finished') { return ( - + showModal({ modal: 'OnBoardingModal' })} + size="large" + disabled={isLoading} + > {isLoading && } {status === 'installPlugin' ? 'Connect Wallet' : 'Join Now'} diff --git a/packages/ui/src/services/i18n/dict/en/settings.json b/packages/ui/src/services/i18n/dict/en/settings.json index 73104a6d1c..e115ede2ad 100644 --- a/packages/ui/src/services/i18n/dict/en/settings.json +++ b/packages/ui/src/services/i18n/dict/en/settings.json @@ -4,15 +4,18 @@ "notifications": "Notifications", "selectNetwork": "Select Network", "networkDetails": "Network Details", - "networkAddress": "Network: ", - "QueryNodeAddress": "Query Node: ", + "networkSubscriptionAddress": "Network subscription: ", + "networkHTTPAddress": "Network HTTP endpoint: ", + "QueryNodeHttpAddress": "Query Node: ", + "QueryNodeSubscriptionAddress": "Query Node subscription: ", "faucet": "Faucet: ", "backend": "Backend: ", "chainInfo": "Chain Information", "rpcBlockheight": "RPC Block Height: ", "qnBlockheight": "Query Node Block Height: ", "customFaucet": "FAUCET", - "customRPCNode": "RPC NODE", + "customWsRpcNode": "RPC NODE SUBSCRIPTION", + "customHttpRpcNode": "RPC NODE HTTP ENDPOINT", "customQueryNode": "QUERY NODE", "customBackend": "BACKEND" -} +} \ No newline at end of file diff --git a/packages/ui/test/app/pages/Settings.test.tsx b/packages/ui/test/app/pages/Settings.test.tsx index 6d84652d47..739bd51f9a 100644 --- a/packages/ui/test/app/pages/Settings.test.tsx +++ b/packages/ui/test/app/pages/Settings.test.tsx @@ -37,8 +37,10 @@ describe('Settings', () => { renderPage() expect(await screen.findByText('selectNetwork')).toBeDefined() expect(await screen.findByText('networkDetails')).toBeDefined() - expect(await screen.findByText('networkAddress')).toBeDefined() - expect(await screen.findByText('QueryNodeAddress')).toBeDefined() + expect(await screen.findByText('networkSubscriptionAddress')).toBeDefined() + expect(await screen.findByText('networkHTTPAddress')).toBeDefined() + expect(await screen.findByText('QueryNodeHttpAddress')).toBeDefined() + expect(await screen.findByText('QueryNodeSubscriptionAddress')).toBeDefined() expect(await screen.findByText('faucet')).toBeDefined() expect(await screen.findByText(mockUseNetworkEndpoints[0].queryNodeEndpoint)).toBeDefined() expect(await screen.findAllByText(mockUseNetworkEndpoints[0].membershipFaucetEndpoint)).toBeDefined() diff --git a/yarn.lock b/yarn.lock index a413a36caa..f142135dce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1997,6 +1997,26 @@ __metadata: languageName: node linkType: hard +"@chainsafe/metamask-polkadot-adapter@npm:^0.6.0": + version: 0.6.0 + resolution: "@chainsafe/metamask-polkadot-adapter@npm:0.6.0" + dependencies: + "@polkadot/api": ^10.9.1 + "@polkadot/extension-inject": ^0.46.5 + "@polkadot/types-augment": ^10.9.1 + checksum: ac8f740b7f57655b03b076b194234c5d2ec3906f1c2cacd8696fa3afc675c5cf19b37e171cef9786b7f8196f8fe88f765f31caf3e359556c6f428978f14a56c6 + languageName: node + linkType: hard + +"@chainsafe/metamask-polkadot-types@npm:^0.6.0": + version: 0.6.0 + resolution: "@chainsafe/metamask-polkadot-types@npm:0.6.0" + dependencies: + "@polkadot/api": ^10.9.1 + checksum: 701c81fa6a59e71175012d0055dcc47810053b1bb1de447509176556ea39a80c541b202060afedad63a8d19a84a4a243ea48d0a9e11d650cfaf20b136d3b7719 + languageName: node + linkType: hard + "@ckeditor/ckeditor5-autoformat@npm:31.0.0": version: 31.0.0 resolution: "@ckeditor/ckeditor5-autoformat@npm:31.0.0" @@ -4868,6 +4888,8 @@ __metadata: "@babel/preset-env": 7 "@babel/preset-react": 7 "@babel/preset-typescript": 7 + "@chainsafe/metamask-polkadot-adapter": ^0.6.0 + "@chainsafe/metamask-polkadot-types": ^0.6.0 "@graphql-codegen/cli": ^2.2.0 "@graphql-codegen/near-operation-file-preset": ^2.1.4 "@graphql-codegen/typescript": ^2.2.2 @@ -6347,6 +6369,24 @@ __metadata: languageName: node linkType: hard +"@polkadot/extension-inject@npm:^0.46.5": + version: 0.46.9 + resolution: "@polkadot/extension-inject@npm:0.46.9" + dependencies: + "@polkadot/api": ^10.12.4 + "@polkadot/rpc-provider": ^10.12.4 + "@polkadot/types": ^10.12.4 + "@polkadot/util": ^12.6.2 + "@polkadot/util-crypto": ^12.6.2 + "@polkadot/x-global": ^12.6.2 + tslib: ^2.6.2 + peerDependencies: + "@polkadot/api": "*" + "@polkadot/util": "*" + checksum: 1072bdad808375ebd2fd154f087f9b99c9c7c547629967d58c67a8228feaf221351b0612008dd1428d94eddc833a4dad69ffca7f8a04dcc7db128154d3ad0971 + languageName: node + linkType: hard + "@polkadot/keyring@npm:12.2.1": version: 12.2.1 resolution: "@polkadot/keyring@npm:12.2.1" @@ -6460,6 +6500,29 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-augment@npm:^10.9.1": + version: 10.13.1 + resolution: "@polkadot/types-augment@npm:10.13.1" + dependencies: + "@polkadot/types": 10.13.1 + "@polkadot/types-codec": 10.13.1 + "@polkadot/util": ^12.6.2 + tslib: ^2.6.2 + checksum: 653ff88c10cc6b6399bd5f54e6fd5c434b7a0e37d3a60d73a7b24a258544aad959907d8d78f347015a2d8006444419d94cd1e5b38c4a20179aba5726407a9998 + languageName: node + linkType: hard + +"@polkadot/types-codec@npm:10.13.1": + version: 10.13.1 + resolution: "@polkadot/types-codec@npm:10.13.1" + dependencies: + "@polkadot/util": ^12.6.2 + "@polkadot/x-bigint": ^12.6.2 + tslib: ^2.6.2 + checksum: 5f5dadd0cde5686c19aab5042180e54bd9496505063bd873014773c6304c57b80903876162a3e87183487570a6a3e69c707b1ca99f4e6272f7c2c1c9588b9b66 + languageName: node + linkType: hard + "@polkadot/types-codec@npm:10.7.1": version: 10.7.1 resolution: "@polkadot/types-codec@npm:10.7.1" @@ -6696,7 +6759,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/x-bigint@npm:^12.2.1": +"@polkadot/x-bigint@npm:^12.2.1, @polkadot/x-bigint@npm:^12.6.2": version: 12.6.2 resolution: "@polkadot/x-bigint@npm:12.6.2" dependencies: @@ -6726,7 +6789,7 @@ __metadata: languageName: node linkType: hard -"@polkadot/x-global@npm:12.6.2, @polkadot/x-global@npm:^12.2.1": +"@polkadot/x-global@npm:12.6.2, @polkadot/x-global@npm:^12.2.1, @polkadot/x-global@npm:^12.6.2": version: 12.6.2 resolution: "@polkadot/x-global@npm:12.6.2" dependencies: From ba71f519e4789a963ec362408d97a6b7df62d509 Mon Sep 17 00:00:00 2001 From: Hugo Lavernhe Date: Sat, 10 Aug 2024 16:13:34 +0200 Subject: [PATCH 2/3] Open `OnBoardingModal` for modals requiring a membership (#4859) * fix: replace modal * add case with wallet --- packages/ui/src/app/GlobalModals.tsx | 22 +++++++++++++------ .../modals/AnnounceCandidacyModal.test.tsx | 10 +++------ .../forum/modals/CreateThreadModal.test.tsx | 6 +---- .../modals/ApplyForRoleModal.test.tsx | 6 +---- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/packages/ui/src/app/GlobalModals.tsx b/packages/ui/src/app/GlobalModals.tsx index 148ac24c03..7ada2bea47 100644 --- a/packages/ui/src/app/GlobalModals.tsx +++ b/packages/ui/src/app/GlobalModals.tsx @@ -3,6 +3,7 @@ import React, { memo, ReactElement, useEffect, useMemo, useState } from 'react' import ReactDOM from 'react-dom' import styled from 'styled-components' +import { useMyAccounts } from '@/accounts/hooks/useMyAccounts' import { ClaimVestingModalCall } from '@/accounts/modals/ClaimVestingModal' import { ClaimVestingModal } from '@/accounts/modals/ClaimVestingModal/ClaimVestingModal' import { MoveFundsModal, MoveFundsModalCall } from '@/accounts/modals/MoveFundsModal' @@ -225,6 +226,7 @@ export const GlobalModals = () => { const { active: activeMember } = useMyMemberships() const { status } = useTransactionStatus() const Modal = useMemo(() => (modal && modal in modals ? memo(() => modals[modal as ModalNames]) : null), [modal]) + const { wallet } = useMyAccounts() const [container, setContainer] = useState(document.body) useEffect(() => { @@ -235,13 +237,19 @@ export const GlobalModals = () => { const potentialFallback = useGlobalModalHandler(currentModalMachine, hideModal) if (modal && !GUEST_ACCESSIBLE_MODALS.includes(modal as ModalNames) && !activeMember) { - showModal({ - modal: 'SwitchMember', - data: { - originalModalName: modal as ModalNames, - originalModalData: modalData, - }, - }) + if (wallet) { + showModal({ + modal: 'SwitchMember', + data: { + originalModalName: modal as ModalNames, + originalModalData: modalData, + }, + }) + } else { + showModal({ + modal: 'OnBoardingModal', + }) + } return null } diff --git a/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx b/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx index d990ea1c85..6daa3e7720 100644 --- a/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx +++ b/packages/ui/test/council/modals/AnnounceCandidacyModal.test.tsx @@ -109,16 +109,12 @@ describe('UI: Announce Candidacy Modal', () => { renderModal() - const switchMemberModalCall = { - modal: 'SwitchMember', - data: { - originalModalName: 'AnnounceCandidateModal', - originalModalData: null, - }, + const onBoardingModalCall = { + modal: 'OnBoardingModal', } expect(showModal).toBeCalledTimes(1) - expect(showModal).toBeCalledWith({ ...switchMemberModalCall }) + expect(showModal).toBeCalledWith({ ...onBoardingModalCall }) }) it('Transaction fee', async () => { diff --git a/packages/ui/test/forum/modals/CreateThreadModal.test.tsx b/packages/ui/test/forum/modals/CreateThreadModal.test.tsx index 286562a419..f59dc32880 100644 --- a/packages/ui/test/forum/modals/CreateThreadModal.test.tsx +++ b/packages/ui/test/forum/modals/CreateThreadModal.test.tsx @@ -80,11 +80,7 @@ describe('CreateThreadModal', () => { useMyMemberships.active = undefined renderModal() expect(useModal.showModal).toBeCalledWith({ - modal: 'SwitchMember', - data: { - originalModalName: 'CreateThreadModal', - originalModalData: useModal.modalData, - }, + modal: 'OnBoardingModal', }) }) diff --git a/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx b/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx index 181a42621d..f412b6383e 100644 --- a/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx +++ b/packages/ui/test/working-groups/modals/ApplyForRoleModal.test.tsx @@ -135,11 +135,7 @@ describe('UI: ApplyForRoleModal', () => { await renderModal() expect(showModal).toBeCalledWith({ - modal: 'SwitchMember', - data: { - originalModalData: modalData, - originalModalName: 'ApplyForRoleModal', - }, + modal: 'OnBoardingModal', }) showModal.mockClear() }) From f1718d35ae7bbf94a2fd28bc579ef2b23b247d73 Mon Sep 17 00:00:00 2001 From: Theophile Sandoz Date: Sat, 10 Aug 2024 16:27:05 +0200 Subject: [PATCH 3/3] Bump version to `3.7.2` --- CHANGELOG.md | 8 +++++++- packages/ui/package.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6932b2fee3..bb9767530d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [3.7.2] - 2024-08-10 + +### Fixed +- Show onboarding flow when a membership is required and no wallet is connected. + ## [3.7.1] - 2024-07-07 ### Fixed @@ -407,7 +412,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.1] - 2022-12-02 -[unreleased]: https://github.com/Joystream/pioneer/compare/v3.7.1...HEAD +[unreleased]: https://github.com/Joystream/pioneer/compare/v3.7.2...HEAD +[3.8.0]: https://github.com/Joystream/pioneer/compare/v3.7.1...v3.7.2 [3.7.1]: https://github.com/Joystream/pioneer/compare/v3.7.0...v3.7.1 [3.7.0]: https://github.com/Joystream/pioneer/compare/v3.6.0...v3.7.0 [3.6.0]: https://github.com/Joystream/pioneer/compare/v3.5.2...v3.6.0 diff --git a/packages/ui/package.json b/packages/ui/package.json index 9064ae8189..d48c3b9986 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@joystream/pioneer", - "version": "3.7.1", + "version": "3.7.2", "license": "GPL-3.0-only", "scripts": { "build": "node --max_old_space_size=4096 ./build.js",