Skip to content

Commit

Permalink
feat: support images in credentials
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Sep 26, 2023
1 parent 7f0b500 commit b058500
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 28 deletions.
10 changes: 6 additions & 4 deletions packages/agent/src/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,11 @@ export function getW3cCredentialForDisplay(w3cCredentialRecord: W3cCredentialRec
const issuerDisplay = getIssuerDisplay(credential, openId4VcMetadata)
const credentialDisplay = getW3cCredentialDisplay(credential, openId4VcMetadata)

// FIXME: support credential with multiple subjects
const credentialAttributes = Array.isArray(credential.credentialSubject)
? credential.credentialSubject[0] ?? {}
: credential.credentialSubject

return {
id: `w3c-credential-${w3cCredentialRecord.id}` satisfies CredentialForDisplayId,
createdAt: w3cCredentialRecord.createdAt,
Expand All @@ -213,9 +218,6 @@ export function getW3cCredentialForDisplay(w3cCredentialRecord: W3cCredentialRec
issuer: issuerDisplay,
},
w3cCredential: credential,
// FIXME: support credential with multiple subjects
attributes: Array.isArray(credential.credentialSubject)
? credential.credentialSubject[0] ?? {}
: credential.credentialSubject,
attributes: credentialAttributes,
}
}
7 changes: 6 additions & 1 deletion packages/app/components/CredentialAttributes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,13 @@ export default function CredentialAttributes({
<TableRow
key={row.key}
attribute={row.key}
value={row.value}
value={
row.type === 'string' || row.type === 'imageAndString' ? row.value : undefined
}
isLastRow={idx === table.rows.length - 1}
image={
row.type === 'image' || row.type === 'imageAndString' ? row.image : undefined
}
/>
))}
</TableContainer>
Expand Down
2 changes: 1 addition & 1 deletion packages/app/features/wallet/WalletScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function WalletScreen() {
})}
</ZStack>
</YStack>
<YStack g="md">
<YStack g="md" marginBottom="$8">
<Heading variant="h3" textAlign="left" secondary>
Credentials
</Heading>
Expand Down
112 changes: 99 additions & 13 deletions packages/app/utils/formatCredentialSubject.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { sanitizeString } from '@internal/utils'

type CredentialAttributeRow = {
export type CredentialAttributeRowString = {
key: string
value: string
type: 'string'
}

type CredentialAttributeTable = {
export type CredentialAttributeRowImage = {
type: 'image'
key: string
image: string
}

export type CredentialAttributeRowImageAndString = {
type: 'imageAndString'
key: string
image: string
value: string
}

type CredentialAttributeRow =
| CredentialAttributeRowString
| CredentialAttributeRowImage
| CredentialAttributeRowImageAndString

export type CredentialAttributeTable = {
title?: string
rows: CredentialAttributeRow[]
depth: number // depth level
Expand Down Expand Up @@ -37,27 +56,94 @@ export function formatCredentialSubject(

if (!value) return // omit properties with no value

if (typeof value === 'string') {
if (typeof value === 'string' && value.startsWith('data:image/')) {
stringRows.push({
key: sanitizeString(key),
image: value,
type: 'image',
})
} else if (typeof value === 'string') {
stringRows.push({
key: sanitizeString(key),
value: value,
type: 'string',
})
// FIXME: Handle arrays
} else if (typeof value === 'object' && value !== null) {
objectTables.push(
...formatCredentialSubject(
value as Record<string, unknown>,
depth + 1,
title,
sanitizeString(key)
}
// FIXME: Handle arrays
else if (typeof value === 'object' && value !== null) {
// Special handling for image
if ('type' in value && value.type === 'Image') {
if ('id' in value && typeof value.id === 'string') {
stringRows.push({
key: sanitizeString(key),
image: value.id,
type: 'image',
})
}
} else {
objectTables.push(
...formatCredentialSubject(
value as Record<string, unknown>,
depth + 1,
title,
sanitizeString(key)
)
)
)
}
}
})

const tableData: CredentialAttributeTable[] = [
{ title, rows: stringRows, depth, parent },
...objectTables,
]
return tableData.filter((table) => table.rows.length > 0)

return tableData
.filter((table) => table.rows.length > 0)
.map((table) => {
const firstImageIndex = table.rows.findIndex((row) => row.type === 'image')
const firstStringIndex = table.rows.findIndex((row) => row.type === 'string')
let rows = table.rows

// This does some fancy logic to combine the first string value and the first image value
// into a combined imageAndString row. When only a single image is present in in an object,
// this will make it look relatively nice, without needing to know the exact structure of
// the credential.
if (
firstImageIndex !== -1 &&
firstStringIndex !== -1 &&
// Due to recursive call, it could be that the rows already contain a combined row
table.rows[0]?.type !== 'imageAndString'
) {
const stringRow = table.rows[firstStringIndex] as CredentialAttributeRowString
const imageRow = table.rows[firstImageIndex] as CredentialAttributeRowImage

const imageAndStringRow = {
type: 'imageAndString',
image: imageRow.image,
key: stringRow.key,
value: stringRow.value,
} satisfies CredentialAttributeRowImageAndString

// Remove the image and string rows and replace with the combined row
rows = [
imageAndStringRow,
...table.rows.filter((row) => row !== imageRow && row !== stringRow),
]
}

rows = rows.sort((a, b) => {
const order = ['imageAndString', 'string', 'image']
return order.indexOf(a.type) - order.indexOf(b.type)
})

return {
...table,
// Sort the rows so that imageAndString rows are first, followed by string rows, followed by image rows
rows: rows.sort((a, b) => {
const order = ['imageAndString', 'string', 'image']
return order.indexOf(a.type) - order.indexOf(b.type)
}),
}
})
}
28 changes: 19 additions & 9 deletions packages/ui/src/table/TableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Paragraph, YStack } from '../base'
import { Paragraph, YStack, XStack } from '../base'
import { Image } from '../content'

interface TableRowProps {
attribute: string
value: string
// Value can be undefined if image prop is used
value?: string
image?: string
isLastRow: boolean
onPress?(): void
}

export const TableRow = ({ attribute, value, isLastRow, onPress }: TableRowProps) => {
export const TableRow = ({ attribute, value, isLastRow, onPress, image }: TableRowProps) => {
const renderedImage = image ? <Image src={image} width={50} height={50} /> : undefined
return (
<YStack
px="$2.5"
Expand All @@ -20,12 +24,18 @@ export const TableRow = ({ attribute, value, isLastRow, onPress }: TableRowProps
opacity: 0.8,
}}
>
<Paragraph variant="text" secondary f={1}>
{attribute}
</Paragraph>
<Paragraph f={1} flexGrow={1}>
{value}
</Paragraph>
<XStack f={1}>
<YStack f={1} justifyContent="flex-start">
<Paragraph variant="text" secondary>
{attribute}
</Paragraph>
{value && <Paragraph>{value}</Paragraph>}
{/* Render image on the left if no value */}
{!value && renderedImage}
</YStack>
{/* Otherwise render image on the right */}
{value && renderedImage}
</XStack>
</YStack>
)
}

0 comments on commit b058500

Please sign in to comment.