Skip to content

Commit

Permalink
🚶‍♀️ WalletConnect integration (Joystream#5867)
Browse files Browse the repository at this point in the history
* mvp of walletconnect integration

* fixed empty class error

* code refactor, visual fixes

* fixed linter issues

* build issues

* fixed signer bugs, fixed reset after refresh, various bugs

* moved chainId to env params

* bugfix, code refactor, mobile implementation, minor fixes

* fixed confirm action

* linter

* prioritise web2 login, enable walletConnect for mobile

* moved WalletConnect deps to atlas package

* cr fixes, bugfix

* fixed disconnect functionality

* lint

* code refactor, secondary button

* fixed wallet reject for nova wallet

* moved towards dynamic set of url

* removed unused var

* unused dep

---------

Co-authored-by: Artem <Artem Slugin>
  • Loading branch information
attemka authored and WRadoslaw committed May 3, 2024
1 parent 714a858 commit d5a3da9
Show file tree
Hide file tree
Showing 24 changed files with 2,228 additions and 107 deletions.
6 changes: 6 additions & 0 deletions packages/atlas/atlas.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,12 @@ features:
members:
avatarServiceUrl: '$VITE_AVATAR_SERVICE_URL' # URL for avatar service - used to upload member's avatar
hcaptchaSiteKey: '$VITE_HCAPTCHA_SITE_KEY' # Site key for hCaptcha - used to verify users are not bots when creating memberships - depends on hCaptcha being enabled in the faucet
walletConnect:
walletConnectProjectId: '$VITE_WALLET_CONNECT_PROJECT_ID' # WalletConnect project ID - used to connect to WalletConnect
metadata:
name: Atlas
description: Web3 video streaming platform
icons: ['https://dev.gleev.xyz/favicon.ico']
playback:
playbackRates: [2, 1.5, 1.25, 1, 0.5, 0.25] # Playback rates available in the player
comments:
Expand Down
2 changes: 2 additions & 0 deletions packages/atlas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
"@sentry/react": "^7.53.1",
"@talismn/connect-wallets": "^1.2.1",
"@tippyjs/react": "^4.2.6",
"@walletconnect/modal": "^2.6.2",
"@walletconnect/universal-provider": "^2.11.1",
"aos": "^2.3.4",
"awesome-debounce-promise": "^2.1.0",
"axios": "^1.2.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/atlas/src/.env
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ VITE_ASSET_LOGS_URL=
VITE_GEOLOCATION_SERVICE_URL=https://geolocation.joystream.org
VITE_HCAPTCHA_SITE_KEY=41cae189-7676-4f6b-aa56-635be26d3ceb
VITE_CHANGENOW_PUBLIC_API_KEY=0d8a58104f82b860a70e9460bccf62ae1e0fca4a93fd7e0c27c90448187b988f
VITE_WALLET_CONNECT_PROJECT_ID=33b2609463e399daee8c51726546c8dd

# YPP configuration
VITE_GOOGLE_CONSOLE_CLIENT_ID=246331758613-rc1psegmsr9l4e33nqu8rre3gno5dsca.apps.googleusercontent.com
Expand Down Expand Up @@ -55,6 +56,7 @@ VITE_NEXT_NODE_URL=wss://3.73.121.180.nip.io/ws-rpc
VITE_NEXT_FAUCET_URL=https://3.73.121.180.nip.io/member-faucet/register
VITE_NEXT_YPP_FAUCET_URL=wss://3.73.121.180.nip.io/ws-rpc


# Local development env URLs
VITE_LOCAL_ORION_AUTH_URL=http://localhost:4074/api/v1
VITE_LOCAL_ORION_URL=http://localhost:4350/graphql
Expand Down
14 changes: 14 additions & 0 deletions packages/atlas/src/assets/icons/WcLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// THIS FILE WAS AUTOGENERATED BY SVGR. DO NOT MODIFY IT MANUALLY;
import { Ref, SVGProps, forwardRef, memo } from 'react'

const SvgWcLogo = forwardRef((props: SVGProps<SVGSVGElement>, ref: Ref<SVGSVGElement>) => (
<svg fill="none" height={24} viewBox="0 0 480 332" width={24} xmlns="http://www.w3.org/2000/svg" ref={ref} {...props}>
<path
d="M126.613 93.984c62.622-61.312 164.152-61.312 226.775 0l7.536 7.379a7.735 7.735 0 0 1 0 11.102l-25.781 25.242a4.07 4.07 0 0 1-5.67 0l-10.371-10.154c-43.687-42.773-114.517-42.773-158.204 0l-11.107 10.874a4.069 4.069 0 0 1-5.669 0l-25.781-25.242a7.733 7.733 0 0 1 0-11.102zm280.093 52.204 22.946 22.465a7.735 7.735 0 0 1 0 11.102L326.189 281.056c-3.131 3.065-8.208 3.065-11.339 0l-73.432-71.896a2.034 2.034 0 0 0-2.835 0l-73.43 71.896c-3.131 3.065-8.208 3.065-11.339 0L50.348 179.754a7.735 7.735 0 0 1 0-11.102l22.946-22.466c3.131-3.065 8.208-3.065 11.339 0l73.433 71.897a2.033 2.033 0 0 0 2.834 0l73.429-71.897c3.131-3.065 8.208-3.065 11.339 0l73.433 71.897a2.034 2.034 0 0 0 2.835 0l73.431-71.895c3.132-3.066 8.208-3.066 11.339 0z"
fill="#3396ff"
/>
</svg>
))
SvgWcLogo.displayName = 'SvgWcLogo'
const Memo = memo(SvgWcLogo)
export { Memo as SvgWcLogo }
1 change: 1 addition & 0 deletions packages/atlas/src/assets/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// THIS FILE WAS AUTOGENERATED BY SVGR. DO NOT MODIFY IT MANUALLY
export * from './WcLogo'
export * from './ActionAddChannel'
export * from './ActionAddImage'
export * from './ActionAddVideo'
Expand Down
1 change: 1 addition & 0 deletions packages/atlas/src/assets/icons/svgs/WC-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Wallet } from '@talismn/connect-wallets'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import shallow from 'zustand/shallow'

import { GetMembershipsQuery, useGetMembershipsLazyQuery } from '@/api/queries/__generated__/memberships.generated'
import { SvgActionNewTab, SvgAlertsError24, SvgAlertsInformative24, SvgLogoPolkadot } from '@/assets/icons'
Expand All @@ -8,8 +9,10 @@ import { AuthenticationModalStepTemplate } from '@/components/_auth/Authenticati
import { Loader } from '@/components/_loaders/Loader'
import { useMediaMatch } from '@/hooks/useMediaMatch'
import { useMountEffect } from '@/hooks/useMountEffect'
import { useAuthStore } from '@/providers/auth/auth.store'
import { UnknownWallet, getWalletsList } from '@/providers/wallet/wallet.helpers'
import { useWallet } from '@/providers/wallet/wallet.hooks'
import { isWalletConnectWallet } from '@/providers/wallet/wallet.types'
import { isMobile } from '@/utils/browser'
import { capitalizeFirstLetter } from '@/utils/misc'

Expand Down Expand Up @@ -41,15 +44,22 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp
const [selectedWalletIdx, setSelectedWalletIdx] = useState<number>(0)
const [hasError, setHasError] = useState(false)
const [isConnecting, setIsConnecting] = useState(false)
const { wallet: walletFromStore, signInToWallet } = useWallet()
const { wallet: walletFromStore, signInToWallet, signInWithWalletConnect } = useWallet()
const [fetchMemberships] = useGetMembershipsLazyQuery({})
const { setAuthModalOpenName } = useAuthStore(
(state) => ({
setAuthModalOpenName: state.actions.setAuthModalOpenName,
}),
shallow
)
const wallets = useMemo(() => {
const unsortedWallets = getWalletsList().filter((wallet) => wallet.installed)

if (isMobileDevice) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rawWallets = Object.keys((window as any).injectedWeb3 || {})

const allMoblieWallets = new Set([
const allMobileWallets = new Set([
'polkawallet',
...rawWallets,
...unsortedWallets
Expand All @@ -58,7 +68,7 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp
.map((wallet) => wallet.extensionName),
])

return Array.from(allMoblieWallets)
return Array.from(allMobileWallets)
.map((walletName) => {
const possiblyInstalledWallet = unsortedWallets.find(
(wallet) => wallet.extensionName === walletName && wallet.extensionName === 'subwallet-js'
Expand All @@ -75,7 +85,7 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp
return {
title: capitalizeFirstLetter(walletName),
extensionName: walletName,
installed: rawWallets.some((rawWallet) => rawWallet === walletName),
installed: [...rawWallets, 'WalletConnect'].some((rawWallet) => rawWallet === walletName),
...(MOBILE_SUPPORTED_WALLETS[walletName as keyof typeof MOBILE_SUPPORTED_WALLETS] ?? { logo: { src: '' } }),
} as UnknownWallet
})
Expand All @@ -91,10 +101,22 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp

setIsConnecting(true)
setHasError(false)
const accounts = await signInToWallet(selectedWallet.extensionName)

const accounts = isWalletConnectWallet(selectedWallet)
? await signInWithWalletConnect().catch((err) => {
const message = err?.message
if (message === 'user_action' || err?.code >= 4000) {
setIsConnecting(false)
return null
}
})
: await signInToWallet(selectedWallet?.extensionName)

if (!accounts) {
setHasError(true)
if (selectedWallet?.extensionName !== 'WalletConnect') {
setHasError(true)
}
setAuthModalOpenName('externalLogIn')
// set error state
return
}
Expand All @@ -106,6 +128,7 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp
},
},
})

setIsConnecting(false)

if (res.data?.memberships.length) {
Expand All @@ -114,7 +137,15 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp
} else {
goToStep(ModalSteps.NoMembership)
}
}, [fetchMemberships, goToStep, selectedWallet, setAvailableMemberships, signInToWallet])
}, [
fetchMemberships,
goToStep,
selectedWallet,
setAuthModalOpenName,
setAvailableMemberships,
signInToWallet,
signInWithWalletConnect,
])

const handleSelectWallet = useCallback((idx: number) => {
setSelectedWalletIdx(idx)
Expand All @@ -126,7 +157,6 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp
if (!walletFromStore) return

const index = wallets.findIndex((w) => w.extensionName === walletFromStore.extensionName)

if (selectedWalletIdx === index) return

setSelectedWalletIdx(index)
Expand All @@ -146,7 +176,7 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp
to: selectedWallet?.installed ? undefined : selectedWallet?.installUrl,
onClick: selectedWallet?.installed ? handleConfirm : undefined,
})
}, [handleConfirm, isConnecting, selectedWallet, setPrimaryButtonProps])
}, [handleConfirm, isConnecting, selectedWallet, setPrimaryButtonProps, wallets.length])

return (
<AuthenticationModalStepTemplate
Expand All @@ -171,7 +201,7 @@ export const ExternalSignInModalWalletStep: FC<ExternalSignInModalWalletStepProp
key={wallet.title}
label={wallet.title}
caption={
wallet.installed
wallet.installed && wallet.extensionName !== 'WalletConnect'
? 'Installed'
: isMobileDevice && wallet.extensionName === 'subwallet-js'
? 'Recommended'
Expand Down
23 changes: 16 additions & 7 deletions packages/atlas/src/components/_auth/LogInModal/LogInModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useForm } from 'react-hook-form'
import { z } from 'zod'
import shallow from 'zustand/shallow'

import { FlexBox } from '@/components/FlexBox'
import { Button, TextButton } from '@/components/_buttons/Button'
import { FormField } from '@/components/_inputs/FormField'
import { Input } from '@/components/_inputs/Input'
Expand Down Expand Up @@ -101,20 +102,28 @@ export const LogInModal = () => {
}}
additionalActionsNode={
!isLoading && (
<Button variant="tertiary" onClick={() => setAuthModalOpenName(undefined)}>
Close
</Button>
<FlexBox justifyContent="space-between">
<Button variant="tertiary" onClick={() => setAuthModalOpenName(undefined)}>
Close
</Button>

<Button variant="secondary" onClick={() => setAuthModalOpenName('externalLogIn')}>
Use local wallet
</Button>
</FlexBox>
)
}
>
{!isLoading ? (
<AuthenticationModalStepTemplate
title="Sign in"
subtitle={
<span>
Use your {atlasConfig.general.appName} account.{' '}
<TextButton onClick={() => setAuthModalOpenName('signUp')}>Create account</TextButton>
</span>
<FlexBox flow="column" alignItems="flex-start">
<span>
Use your {atlasConfig.general.appName} account.{' '}
<TextButton onClick={() => setAuthModalOpenName('signUp')}>Create account</TextButton>
</span>
</FlexBox>
}
hasNavigatedBack
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export const TransactionModal: FC<TransactionModalProps> = ({ onClose, status, c
}
}, [decrementOverlaysOpenCount])

// @ts-ignore different wallet types before lib integration
const walletLogo = wallet?.logo ? wallet.logo.src : wallet.metadata.logoUrl || null

return (
<StyledModal show={!!stepDetails} className={className}>
<StepsBar>
Expand Down Expand Up @@ -116,7 +119,7 @@ export const TransactionModal: FC<TransactionModalProps> = ({ onClose, status, c
>
<WalletInfoWrapper>
<StyledIconWrapper
icon={wallet?.logo.src ? <WalletLogo src={wallet.logo.src} alt={wallet.logo.alt} /> : <SvgLogoPolkadot />}
icon={walletLogo ? <WalletLogo src={walletLogo} alt={wallet?.logo?.alt} /> : <SvgLogoPolkadot />}
/>
<Text as="span" color="colorText" variant="t100">
Continue in {wallet?.title}
Expand Down
8 changes: 8 additions & 0 deletions packages/atlas/src/config/configSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ export const configSchema = z.object({
avatarServiceUrl: z.string(),
hcaptchaSiteKey: z.string().nullable(),
}),
walletConnect: z.object({
walletConnectProjectId: z.string().nullable(),
metadata: z.object({
name: z.string().nullable(),
description: z.string().nullable(),
icons: z.array(z.string()).nullable(),
}),
}),
playback: z.object({ playbackRates: z.array(z.number()) }),
comments: z.object({
reactions: z.array(z.object({ id: z.number(), emoji: z.string(), name: z.string() })),
Expand Down
6 changes: 4 additions & 2 deletions packages/atlas/src/providers/auth/auth.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ export const loginRequest = (
export const logoutRequest = () => axiosInstance.post(`${ORION_AUTH_URL}/logout`, {}, { withCredentials: true })

export const getCorrectLoginModal = (): AuthModals => {
const hasAtleastOneWallet = getWalletsList().some((wallet) => wallet.installed)
return hasAtleastOneWallet ? 'externalLogIn' : 'logIn'
const hasAtLeastOneWallet = getWalletsList().some(
(wallet) => wallet.extensionName !== 'WalletConnect' && wallet.installed
)
return hasAtLeastOneWallet ? 'externalLogIn' : 'logIn'
}

export const prepareEncryptionArtifacts = async (email: string, password: string, mnemonic: string) => {
Expand Down
13 changes: 12 additions & 1 deletion packages/atlas/src/providers/auth/auth.provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { useAuthStore } from '@/providers/auth/auth.store'
import { useJoystream } from '@/providers/joystream/joystream.provider'
import { useWallet } from '@/providers/wallet/wallet.hooks'
import { useWalletStore } from '@/providers/wallet/wallet.store'
import { isWalletConnectWallet } from '@/providers/wallet/wallet.types'
import { SentryLogger } from '@/utils/logs'

import {
Expand Down Expand Up @@ -44,6 +45,7 @@ export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
actions: { setAnonymousUserId, setEncodedSeed },
} = useAuthStore()
const lastUsedWalletName = useWalletStore((store) => store.lastUsedWalletName)
const currentWallet = useWalletStore((store) => store.wallet)
const { signInToWallet } = useWallet()

useMountEffect(() => {
Expand Down Expand Up @@ -247,6 +249,15 @@ export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {

const handleLogout: AuthContextValue['handleLogout'] = useCallback(async () => {
try {
if (currentWallet && isWalletConnectWallet(currentWallet)) {
await currentWallet?.client?.disconnect({
topic: currentWallet?.session?.topic || '',
reason: {
code: -1,
message: 'Disconnected by client!',
},
})
}
await logoutRequest()
handleAnonymousAuth(anonymousUserId).then((userId) => {
setAnonymousUserId(userId ?? null)
Expand All @@ -257,7 +268,7 @@ export const AuthProvider: FC<PropsWithChildren> = ({ children }) => {
} catch (error) {
SentryLogger.error('Error when logging out', 'auth.provider', error)
}
}, [anonymousUserId, setAnonymousUserId, setEncodedSeed, trackLogout])
}, [anonymousUserId, currentWallet, setAnonymousUserId, setEncodedSeed, trackLogout])

const isWalletUser = useMemo(() => encodedSeed === null && !!currentUser, [currentUser, encodedSeed])

Expand Down
4 changes: 4 additions & 0 deletions packages/atlas/src/providers/wallet/tmpwallet/consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const JOYSTREAM_DEFAULT_CHAIN_ID = 'polkadot:6b5e488e0fa8f9821110d5c13f4c468a'
export const WC_VERSION = '2.0'

export const JOYSTREAM_SS58_PREFIX = 126
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types.js'
43 changes: 43 additions & 0 deletions packages/atlas/src/providers/wallet/tmpwallet/core/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Signer } from '@polkadot/types/types'

export type KeypairType = 'ed25519' | 'sr25519'

export interface Account {
address: string
type?: KeypairType
genesisHash?: string | null
name?: string
}

export enum WalletType {
INJECTED = 'INJECTED',
WALLET_CONNECT = 'WALLET_CONNECT',
LEDGER = 'LEDGER',
}

export interface BaseWalletProvider {
getWallets: () => Promise<BaseWallet[]>
}

export interface WalletMetadata {
id: string
title: string
description?: string
urls?: { main?: string; browsers?: Record<string, string> }
iconUrl?: string
version?: string
}

export type UnsubscribeFn = () => void

export interface BaseWallet {
metadata: WalletMetadata
type: WalletType
// signer will be available when the wallet is connected, otherwise it is undefined
signer: Signer | undefined
connect: () => Promise<void>
disconnect: () => Promise<void>
isConnected: () => boolean
getAccounts: () => Promise<Account[]>
subscribeAccounts: (cb: (accounts: Account[]) => void) => Promise<UnsubscribeFn>
}
3 changes: 3 additions & 0 deletions packages/atlas/src/providers/wallet/tmpwallet/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './wallet-connect.js'
export * from './types.js'
export * from './consts.js'
Loading

0 comments on commit d5a3da9

Please sign in to comment.