Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support images in credentials #68

Merged
merged 1 commit into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
)
}