Skip to content

Commit

Permalink
Auth structure and LogIn flow (mocked) (#5)
Browse files Browse the repository at this point in the history
* feat add sign in page and auth module

* remove env

* remove env

* fix after review

* feat add setting userDid to mm-zkp context

* feat move MMlinkSwitcher to heplers

* feat add auth hook

* feat: refatore auth module and fix icon

* fix rename helper

* fix sign in

* fixes after review

* fix: refactore auth hook
  • Loading branch information
WhiteNik16 authored Dec 28, 2023
1 parent 9b53188 commit 2849088
Show file tree
Hide file tree
Showing 18 changed files with 256 additions and 50 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ stats.html
.env.development
.env.production
.env.local
.env

# Sentry Auth Token
.env.sentry-build-plugin
Expand Down
23 changes: 20 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ import { CircularProgress, CssBaseline, Stack, ThemeProvider } from '@mui/materi
import { FC, HTMLAttributes, memo, useCallback, useEffect, useState } from 'react'

import { ErrorHandler } from '@/helpers'
import { useMetamaskZkpSnapContext, useThemeMode, useViewportSizes, useWeb3Context } from '@/hooks'
import {
useAuth,
useMetamaskZkpSnapContext,
useThemeMode,
useViewportSizes,
useWeb3Context,
} from '@/hooks'

const App: FC<HTMLAttributes<HTMLDivElement>> = ({ children }) => {
const [isAppInitialized, setIsAppInitialized] = useState(false)

const { provider, isValidChain, init: initWeb3 } = useWeb3Context()
const { theme } = useThemeMode()
const { checkMetamaskExists, checkSnapExists, connectOrInstallSnap } = useMetamaskZkpSnapContext()
const { authorize } = useAuth()

useViewportSizes()

Expand All @@ -23,14 +30,24 @@ const App: FC<HTMLAttributes<HTMLDivElement>> = ({ children }) => {
* because only want to check is user was connected before
*/
await initWeb3()
if (await checkSnapExists()) await connectOrInstallSnap()
if (await checkSnapExists()) {
await connectOrInstallSnap()
await authorize()
}
}
} catch (error) {
ErrorHandler.processWithoutFeedback(error)
}

setIsAppInitialized(true)
}, [provider?.address, checkMetamaskExists, initWeb3, checkSnapExists, connectOrInstallSnap])
}, [
provider?.address,
checkMetamaskExists,
initWeb3,
checkSnapExists,
connectOrInstallSnap,
authorize,
])

useEffect(() => {
let mountingInit = async () => {
Expand Down
7 changes: 7 additions & 0 deletions src/assets/icons/user-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 0 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ export type Config = {
SUPPORTED_CHAINS_DETAILS: SupportedChainsDetails
DEFAULT_CHAIN: SupportedChains
ROBOTORNOT_LINK: string
CHROME_METAMASK_ADDON_LINK: string
FIREFOX_METAMASK_ADDON_LINK: string
OPERA_METAMASK_ADDON_LINK: string
}

export const config: Config = {
Expand All @@ -27,7 +24,4 @@ export const config: Config = {
SUPPORTED_CHAINS_DETAILS,
DEFAULT_CHAIN: import.meta.env.VITE_DEFAULT_CHAIN,
ROBOTORNOT_LINK: 'https://robotornot.mainnet-beta.rarimo.com/',
CHROME_METAMASK_ADDON_LINK: 'https://chrome.google.com/webstore/detail/metamask/',
FIREFOX_METAMASK_ADDON_LINK: 'https://addons.mozilla.org/en-US/firefox/addon/ether-metamask/',
OPERA_METAMASK_ADDON_LINK: 'https://addons.opera.com/en/extensions/details/metamask-10/',
}
14 changes: 12 additions & 2 deletions src/contexts/metamask-zkp-snap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { createContext, FC, HTMLAttributes, useCallback, useState } from 'react'
interface MetamaskZkpSnapContextValue {
isMetamaskInstalled: boolean
isSnapInstalled: boolean
userDid: string
userDidBigIntString: string

isLocalSnap: (snapId: string) => boolean

Expand Down Expand Up @@ -45,6 +47,8 @@ const CONTEXT_NOT_INITIALIZED_ERROR = new ReferenceError('MetamaskZkpSnapContext
export const MetamaskZkpSnapContext = createContext<MetamaskZkpSnapContextValue>({
isMetamaskInstalled: false,
isSnapInstalled: false,
userDid: '',
userDidBigIntString: '',

isLocalSnap: () => {
throw CONTEXT_NOT_INITIALIZED_ERROR
Expand Down Expand Up @@ -82,6 +86,8 @@ export const MetamaskZkpSnapContextProvider: FC<HTMLAttributes<HTMLDivElement>>

const [isMetamaskInstalled, setIsMetamaskInstalled] = useState(false)
const [isSnapInstalled, setIsSnapInstalled] = useState(false)
const [userDid, setUserDid] = useState('')
const [userDidBigIntString, setUserDidBigIntString] = useState('')

const isLocalSnap = useCallback((snapId: string) => snapId.startsWith('local:'), [])

Expand All @@ -91,8 +97,10 @@ export const MetamaskZkpSnapContextProvider: FC<HTMLAttributes<HTMLDivElement>>
*/
const createIdentity = useCallback(async () => {
if (!connector) throw new TypeError('Connector is not defined')

return connector.createIdentity()
const identity = await connector.createIdentity()
setUserDid(identity.identityIdString)
setUserDidBigIntString(identity.identityIdBigIntString)
return identity
}, [connector])

/**
Expand Down Expand Up @@ -163,6 +171,8 @@ export const MetamaskZkpSnapContextProvider: FC<HTMLAttributes<HTMLDivElement>>
value={{
isMetamaskInstalled,
isSnapInstalled,
userDid,
userDidBigIntString,

isLocalSnap,

Expand Down
1 change: 1 addition & 0 deletions src/enums/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { default as Work } from '@mui/icons-material/Work'

export enum Icons {
Metamask = 'metamask',
User = 'user',
Wallet = 'wallet',
}

Expand Down
1 change: 0 additions & 1 deletion src/enums/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './bus'
export * from './icons'
export * from './locals-storage-keys'
export * from './routes'
export * from './theme'
4 changes: 0 additions & 4 deletions src/enums/locals-storage-keys.ts

This file was deleted.

1 change: 1 addition & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './error-handler'
export * from './event-bus'
export * from './metamask'
export * from './promise'
export * from './store'
30 changes: 30 additions & 0 deletions src/helpers/metamask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { get } from 'lodash'

const OTHER_BROWSER_METAMASK_LINK = 'https://metamask.io/download/'
const CHROME_METAMASK_ADDON_LINK = 'https://chrome.google.com/webstore/detail/metamask/'
const FIREFOX_METAMASK_ADDON_LINK = 'https://addons.mozilla.org/en-US/firefox/addon/ether-metamask/'
const OPERA_METAMASK_ADDON_LINK = 'https://addons.opera.com/en/extensions/details/metamask-10/'

export function metamaskLink() {
const browserExtensionsLinks = {
chrome: CHROME_METAMASK_ADDON_LINK,
opera: OPERA_METAMASK_ADDON_LINK,
firefox: FIREFOX_METAMASK_ADDON_LINK,
}

// Get the user-agent string
const userAgentString = navigator.userAgent

let chromeAgent = userAgentString.indexOf('Chrome') > -1 ? 'chrome' : ''
const firefoxAgent = userAgentString.indexOf('Firefox') > -1 ? 'firefox' : ''
const operaAgent = userAgentString.indexOf('OP') > -1 ? 'opera' : ''

// Discard Chrome since it also matches Opera
if (chromeAgent && operaAgent) chromeAgent = ''

const currentBrowser = chromeAgent || firefoxAgent || operaAgent || ''

if (!currentBrowser) return OTHER_BROWSER_METAMASK_LINK

return get(browserExtensionsLinks, currentBrowser, '')
}
72 changes: 72 additions & 0 deletions src/hooks/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { PROVIDERS } from '@distributedlab/w3p'
import { useCallback, useMemo, useState } from 'react'

import { useMetamaskZkpSnapContext } from '@/hooks/metamask-zkp-snap'
import { useWeb3Context } from '@/hooks/web3'
import { authStore, useAuthState } from '@/store'

export const useAuth = () => {
const { jwt: storeJwt } = useAuthState()
const { init, provider } = useWeb3Context()
const { connectOrInstallSnap, isSnapInstalled } = useMetamaskZkpSnapContext()
const [isJwtValid, setIsJwtValid] = useState(false)

const isAuthorized = useMemo(
() => provider?.isConnected && isSnapInstalled && isJwtValid,
[isJwtValid, isSnapInstalled, provider?.isConnected],
)

const _setJwt = useCallback((jwt: string) => {
authStore.setJwt(jwt)
}, [])

const checkJwtValid = useCallback(async () => {
//Todo: add real logic
return true
}, [])

const logOut = useCallback(async () => {
await provider?.disconnect()
_setJwt('')
setIsJwtValid(false)
}, [_setJwt, provider])

const authorize = useCallback(
async (jwt?: string) => {
const currentJwt = jwt || storeJwt

if (!currentJwt) await logOut()

const isJwtValid = await checkJwtValid()

if (isJwtValid) {
setIsJwtValid(true)
_setJwt(currentJwt)
return
}

logOut()

// TODO: Replace with real auth check
},
[_setJwt, checkJwtValid, storeJwt, logOut],
)

const login = useCallback(async () => {
await init(PROVIDERS.Metamask)
await connectOrInstallSnap()
// TODO: generateProof and /login
const jwt = 'mockJwt'

await authorize(jwt)
}, [authorize, connectOrInstallSnap, init])

return {
isAuthorized,
storeJwt,
login,
authorize,
logOut,
checkJwtValid,
}
}
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './auth'
export * from './form'
export * from './interval'
export * from './loading'
Expand Down
5 changes: 4 additions & 1 deletion src/locales/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
},
"sign-in-page": {
"title": "Sign in",
"description": "Manage your identity credentials and Soulbound Tokens (SBTs) easily from this dashboard"
"description": "Manage your identity credentials and Soulbound Tokens (SBTs) easily from this dashboard",
"connect-btn": "Connect Metamask",
"install-btn": "Install Metamask",
"reload-page-btn": "Please, reload page"
},
"validations": {
"field-error_required": "Please fill out this field",
Expand Down
98 changes: 76 additions & 22 deletions src/pages/SignIn.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,89 @@
import { PROVIDERS } from '@distributedlab/w3p'
import { Stack, Typography } from '@mui/material'
import { useEffect } from 'react'
import { Stack, Typography, useTheme } from '@mui/material'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'

import { Routes } from '@/enums'
import { useMetamaskZkpSnapContext, useWeb3Context } from '@/hooks'
import { UiButton } from '@/ui'
import { BusEvents, Icons } from '@/enums'
import { bus, ErrorHandler, metamaskLink } from '@/helpers'
import { useAuth, useMetamaskZkpSnapContext } from '@/hooks'
import { UiButton, UiIcon } from '@/ui'

export default function SignIn() {
const navigate = useNavigate()
const { t } = useTranslation()
const { init: initWeb3, provider } = useWeb3Context()
const { connectOrInstallSnap } = useMetamaskZkpSnapContext()
const { login } = useAuth()
const [isPending, setIsPending] = useState(false)

const connectWallet = async () => {
await initWeb3(PROVIDERS.Metamask)
await connectOrInstallSnap()
}
const { palette } = useTheme()
const { isMetamaskInstalled } = useMetamaskZkpSnapContext()

useEffect(() => {
if (provider?.isConnected) {
navigate(Routes.Profiles)
const signIn = useCallback(async () => {
setIsPending(true)

try {
await login()
} catch (error) {
ErrorHandler.process(error)
}
}, [navigate, provider?.isConnected])

setIsPending(false)
}, [login])

const installMMLink = useMemo(() => {
if (isMetamaskInstalled) return ''

return metamaskLink()
}, [isMetamaskInstalled])

const openInstallMetamaskLink = useCallback(() => {
if (!installMMLink) {
bus.emit(BusEvents.warning, `Your browser is not support Metamask`)

return
}

setIsPending(true)

window.open(installMMLink, '_blank', 'noopener noreferrer')
}, [installMMLink])

return (
<Stack flex={1}>
<Typography>{t('sign-in-page.title')}</Typography>
<Typography>{t('sign-in-page.description')}</Typography>
<UiButton onClick={connectWallet}>Connect</UiButton>
<Stack
maxWidth={520}
borderRadius={4}
p={16}
flexDirection='column'
alignItems='center'
bgcolor={palette.background.paper}
>
<UiIcon
size={22}
name={Icons.User}
sx={{ background: palette.background.default, borderRadius: 100 }}
color={palette.primary.main}
/>
{/*Todo: add metamask not found texts*/}
<Typography component='h4' variant='h4' sx={{ my: 4 }}>
{t('sign-in-page.title')}
</Typography>
<Typography variant='body2' marginBottom={8} textAlign={'center'}>
{t('sign-in-page.description')}
</Typography>
{isMetamaskInstalled ? (
<UiButton
onClick={signIn}
startIcon={<UiIcon name={Icons.Metamask} />}
disabled={isPending}
>
{t('sign-in-page.connect-btn')}
</UiButton>
) : (
<UiButton
onClick={openInstallMetamaskLink}
startIcon={<UiIcon name={Icons.Metamask} />}
disabled={isPending}
>
{isPending ? t('sign-in-page.reload-page-btn') : t('sign-in-page.install-btn')}
</UiButton>
)}
</Stack>
)
}
Loading

0 comments on commit 2849088

Please sign in to comment.