Skip to content

Commit

Permalink
Credentials list and single page (#41)
Browse files Browse the repository at this point in the history
* update metadata interaction

* credentials list, item and card

* upd credentials pages design, add rm vc method

* add interactions, handlers and ui components

* add issuer details and vc formatting

* load credentials via store
  • Loading branch information
lukachi authored Feb 13, 2024
1 parent d8ee1e2 commit e233387
Show file tree
Hide file tree
Showing 32 changed files with 804 additions and 149 deletions.
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog], and this project adheres to [Semantic Versioning].

## [Unreleased]

## Unreleased
### Added
- Initiated project

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"jdenticon": "^3.2.0",
"lodash": "^4.17.21",
"loglevel": "^1.8.1",
"material-ui-popup-state": "^5.0.10",
"notistack": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ToastsManager } from '@/contexts'
import { ErrorHandler } from '@/helpers'
import { useAuth, useViewportSizes, useWeb3Context } from '@/hooks'
import { AppRoutes } from '@/routes'
import { useUiState, web3Store } from '@/store'
import { credentialsStore, useUiState, web3Store } from '@/store'
import { createTheme } from '@/theme'

const App: FC<HTMLAttributes<HTMLDivElement>> = () => {
Expand All @@ -23,6 +23,8 @@ const App: FC<HTMLAttributes<HTMLDivElement>> = () => {

if (isMetamaskInstalled && isSnapInstalled) {
await connectProviders()

await credentialsStore.load()
}
} catch (error) {
ErrorHandler.processWithoutFeedback(error)
Expand Down
2 changes: 1 addition & 1 deletion src/api/clients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export const api = new JsonApiClient({
export let zkpSnap: SnapConnector

export const initZkpSnap = async () => {
const snap = await enableSnap()
const snap = await enableSnap(...config.SNAP_V_PARAMS)
zkpSnap = await snap.getConnector()
}
12 changes: 8 additions & 4 deletions src/api/modules/orgs/helpers/org-groups-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,16 @@ export const loadOrgGroupRequests = async (query?: OrgGroupRequestQueryParams) =
return fakeLoadRequestsAll(query)
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const loadRequestsByUserDid = async (did: string): Promise<OrgGroupRequestWithClaims[]> => {
const { data } = await api.get<OrgGroupRequestWithClaims[]>(
`${ApiServicePaths.Orgs}/v1/users/${did}/requests`,
)
// TODO: return once backend is ready
// const { data } = await api.get<OrgGroupRequestWithClaims[]>(
// `${ApiServicePaths.Orgs}/v1/users/${did}/requests`,
// )
//
// return data

return data
return []
}

export const loadOrgGroupRequestById = async (orgId: string, groupId: string, reqId: string) => {
Expand Down
35 changes: 31 additions & 4 deletions src/api/modules/zkp/helpers/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { fetcher } from '@distributedlab/fetcher'
import { SaveCredentialsRequestParams, W3CCredential } from '@rarimo/rarime-connector'
import omit from 'lodash/omit'
import startCase from 'lodash/startCase'

import { api } from '@/api/clients'
import {
CredentialSubject,
IssuerDetails,
JsonLdSchema,
ParsedCredentialSchema,
ParsedCredentialSchemaProperty,
Expand Down Expand Up @@ -70,13 +72,38 @@ export const getTargetProperty = (
}

export const getClaimIdFromVC = (credential: W3CCredential) => {
let claimId = ''

try {
const claimIdUrl = new URL(credential.id)

const pathNameParts = claimIdUrl.pathname.split('/')

return pathNameParts[pathNameParts.length - 1]
claimId = claimIdUrl.pathname.split('/').pop() ?? ''
} catch (error) {
return credential.id
/* empty */
}

return claimId || credential.id
}

export const getIssuerDetails = async (issuerDid: string): Promise<IssuerDetails> => {
// TODO: This is a temporary solution, we need to get issuer details from the backend
return {
did: issuerDid,
name: 'Rarimo',
}
}

export const getCredentialViewProperty = (vc: W3CCredential) => {
// TODO: This is a temporary solution, we need to get VC view property
return vc.credentialSubject.provider as string
}

export const formatCredentialType = (vcType: string[]) => {
switch (vcType[1]) {
case 'IdentityProviders': {
return 'Proof of Human'
}
default:
return startCase(vcType[1])
}
}
5 changes: 5 additions & 0 deletions src/api/modules/zkp/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,8 @@ export type ParsedCredentialSchema = {
type: string
credSubjectProperties: ParsedCredentialSchemaProperty[]
}

export type IssuerDetails = {
did: string
name: string
}
120 changes: 120 additions & 0 deletions src/common/CredentialCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { alpha, Box, Divider, Stack, StackProps, Typography, useTheme } from '@mui/material'
import { W3CCredential } from '@rarimo/rarime-connector'
import startCase from 'lodash/startCase'

import { formatCredentialType, getCredentialViewProperty, IssuerDetails } from '@/api/modules/zkp'
import { formatDateMY } from '@/helpers'
import { UiIcon } from '@/ui'

function DotsDecoration({ ...rest }: StackProps) {
const { palette } = useTheme()

const rowsCount = 3
const maxDots = 5
const betweenDotsSpacing = 2

// TODO: alpha(palette.common.white, ...) should be configured together with bg
return (
<Stack {...rest} alignItems={'flex-end'} spacing={betweenDotsSpacing}>
{Array.from({ length: rowsCount }, (v, i) => i).map(rowIdx => (
<Stack key={rowIdx} direction='row' spacing={betweenDotsSpacing}>
{Array.from({ length: maxDots - rowIdx }, (v, i) => i).map(boxIdx => (
<Box
key={boxIdx}
width={8}
height={8}
borderRadius={'50%'}
bgcolor={alpha(palette.common.white, 0.16)}
/>
))}
</Stack>
))}
</Stack>
)
}

type Props = StackProps & {
vc: W3CCredential
issuerDetails: IssuerDetails
}

export default function CredentialCard({ vc, issuerDetails, ...rest }: Props) {
const { palette, spacing } = useTheme()

return (
<Stack
{...rest}
spacing={6}
p={6}
borderRadius={4}
sx={{
position: 'relative',
width: '100%',
// TODO: use background from vc metadata or predefined bg map
background: 'linear-gradient(#252C3B 100%, #0F1218 100%)',
}}
>
<DotsDecoration
sx={{
position: 'absolute',
top: spacing(6),
right: spacing(6),
}}
/>
<Box
width={40}
height={40}
color={palette.common.white}
bgcolor={alpha(palette.common.white, 0.1)}
borderRadius={'50%'}
p={2}
>
{/*TODO: define map for credential types*/}
<UiIcon componentName='fingerprint' />
</Box>

<Stack spacing={2}>
<Typography variant='h6' color={palette.common.white}>
{formatCredentialType(vc.type)}
</Typography>

<Box
sx={{
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '50%',
color: alpha(palette.common.white, 0.6),
}}
>
<Typography variant='body3'>{issuerDetails.name}</Typography>
</Box>
</Stack>

<Divider
sx={{
backgroundColor: alpha(palette.common.white, 0.1),
}}
/>
<Stack spacing={2} direction='row' alignItems='center' justifyContent='space-between'>
<Typography variant='body4' color={alpha(palette.common.white, 0.6)}>
<Stack direction='row' alignItems='center' spacing={2}>
{vc.expirationDate ? (
<>
<UiIcon componentName='calendarTodayOutlinedIcon' size={4} />
<Typography>{formatDateMY(vc.expirationDate)}</Typography>
</>
) : (
<UiIcon componentName='allInclusiveOutlinedIcon' size={4} />
)}
</Stack>
</Typography>

<Typography variant='body4' color={alpha(palette.common.white, 0.6)}>
<Stack direction='row' alignItems='center' spacing={2}>
<Typography>{startCase(getCredentialViewProperty(vc))}</Typography>
</Stack>
</Typography>
</Stack>
</Stack>
)
}
1 change: 1 addition & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as AppNavbar } from './AppNavbar'
export { default as CredentialCard } from './CredentialCard'
export { default as FillRequestForm } from './FillRequestForm'
export { default as NoDataViewer } from './NoDataViewer'
export { default as PageListFilters } from './PageListFilters'
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type Config = {
DEFAULT_CHAIN: SupportedChains
ROBOTORNOT_LINK: string
SUPPORT_LINK: string
SNAP_V_PARAMS: string[]
}

const FALLBACK_DEFAULT_CHAIN = Object.entries(FALLBACK_SUPPORTED_CHAINS)[0][0]
Expand All @@ -28,4 +29,5 @@ export const config: Config = {
DEFAULT_CHAIN: import.meta.env.VITE_DEFAULT_CHAIN || FALLBACK_DEFAULT_CHAIN,
ROBOTORNOT_LINK: 'https://robotornot.mainnet-beta.rarimo.com/',
SUPPORT_LINK: 'https://rarime.com',
SNAP_V_PARAMS: [], // ['local:http://localhost:8081', '2.1.0-rc.2'],
}
10 changes: 10 additions & 0 deletions src/enums/icons.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import AccountCircleIcon from '@mui/icons-material/AccountCircle'
import Add from '@mui/icons-material/Add'
import AddPhotoAlternateOutlined from '@mui/icons-material/AddPhotoAlternateOutlined'
import AllInclusiveOutlinedIcon from '@mui/icons-material/AllInclusiveOutlined'
import ArrowForward from '@mui/icons-material/ArrowForward'
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined'
import CheckIcon from '@mui/icons-material/Check'
import ChevronLeft from '@mui/icons-material/ChevronLeft'
import ChevronRight from '@mui/icons-material/ChevronRight'
import Close from '@mui/icons-material/Close'
import ContentCopy from '@mui/icons-material/ContentCopy'
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined'
Expand All @@ -12,6 +15,7 @@ import DeleteOutlined from '@mui/icons-material/DeleteOutlined'
import DragIndicator from '@mui/icons-material/DragIndicator'
import DriveFileRenameOutlineOutlined from '@mui/icons-material/DriveFileRenameOutlineOutlined'
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'
import FingerPrint from '@mui/icons-material/Fingerprint'
import FolderOff from '@mui/icons-material/FolderOff'
import InfoIcon from '@mui/icons-material/Info'
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
Expand All @@ -21,6 +25,7 @@ import Layers from '@mui/icons-material/Layers'
import LightModeOutlined from '@mui/icons-material/LightModeOutlined'
import Link from '@mui/icons-material/Link'
import Logout from '@mui/icons-material/Logout'
import MoreHoriz from '@mui/icons-material/MoreHoriz'
import Notifications from '@mui/icons-material/Notifications'
import OpenInNew from '@mui/icons-material/OpenInNew'
import QrCode from '@mui/icons-material/QrCode'
Expand Down Expand Up @@ -48,6 +53,7 @@ export const ICON_COMPONENTS = {
arrowForward: ArrowForward,
check: CheckIcon,
chevronLeft: ChevronLeft,
chevronRight: ChevronRight,
contentCopy: ContentCopy,
close: Close,
darkModeOutlined: DarkModeOutlined,
Expand All @@ -73,4 +79,8 @@ export const ICON_COMPONENTS = {
verified: Verified,
warningAmber: WarningAmberIcon,
work: Work,
fingerprint: FingerPrint,
calendarTodayOutlinedIcon: CalendarTodayOutlinedIcon,
allInclusiveOutlinedIcon: AllInclusiveOutlinedIcon,
moreHoriz: MoreHoriz,
}
1 change: 1 addition & 0 deletions src/enums/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ export enum RoutePaths {

Credentials = '/credentials',
CredentialsList = '/credentials/list',
CredentialsId = '/credentials/:claimId',
CredentialsRequests = '/credentials/requests',
}
6 changes: 6 additions & 0 deletions src/helpers/format.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { time } from '@distributedlab/tools'

const FORMATTED_DID_MAX_LENGTH = 12

export function formatDid(did: string) {
return did.length > FORMATTED_DID_MAX_LENGTH ? did.slice(0, 8) + '...' + did.slice(-4) : did
}

export function formatDateMY(date: string) {
return time(date).format('MM / YYYY')
}
21 changes: 15 additions & 6 deletions src/helpers/store.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { INTERNAL_Snapshot, proxy, subscribe, useSnapshot } from 'valtio'
import { proxy, subscribe, useSnapshot } from 'valtio'

type CreateStoreOpts = {
isPersist: boolean
}

export const createStore = <S extends object, A>(
storeName: string,
initialState: S,
actions: (state: S) => A,
): [Readonly<S> & A, () => INTERNAL_Snapshot<S>] => {
opts: CreateStoreOpts = {
isPersist: true,
},
): [Readonly<S> & A, () => S] => {
const storageState = localStorage.getItem(storeName)

let parsedStorageState: S = {} as S
Expand All @@ -20,9 +27,11 @@ export const createStore = <S extends object, A>(
...parsedStorageState,
})

subscribe(state, () => {
localStorage.setItem(storeName, JSON.stringify(state))
})
if (opts?.isPersist) {
subscribe(state, () => {
localStorage.setItem(storeName, JSON.stringify(state))
})
}

return [Object.assign(state, actions(state)), () => useSnapshot(state)]
return [Object.assign(state, actions(state)), () => useSnapshot(state) as S]
}
3 changes: 3 additions & 0 deletions src/locales/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,8 @@
"org-list": {
"title": "Organizations",
"subtitle": "Manage your identity credentials and Soulbound Tokens (SBTs) easily from this app"
},
"credentials-list": {
"title": "Credentials"
}
}
Loading

0 comments on commit e233387

Please sign in to comment.