From b058500ea420a9e2f43d03416a8913a22dc4eed4 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 26 Sep 2023 14:15:10 +0200 Subject: [PATCH] feat: support images in credentials Signed-off-by: Timo Glastra --- packages/agent/src/display.ts | 10 +- .../app/components/CredentialAttributes.tsx | 7 +- packages/app/features/wallet/WalletScreen.tsx | 2 +- packages/app/utils/formatCredentialSubject.ts | 112 ++++++++++++++++-- packages/ui/src/table/TableRow.tsx | 28 +++-- 5 files changed, 131 insertions(+), 28 deletions(-) diff --git a/packages/agent/src/display.ts b/packages/agent/src/display.ts index af4b3f9f..17da0f9d 100644 --- a/packages/agent/src/display.ts +++ b/packages/agent/src/display.ts @@ -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, @@ -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, } } diff --git a/packages/app/components/CredentialAttributes.tsx b/packages/app/components/CredentialAttributes.tsx index 98988648..0b53d399 100644 --- a/packages/app/components/CredentialAttributes.tsx +++ b/packages/app/components/CredentialAttributes.tsx @@ -48,8 +48,13 @@ export default function CredentialAttributes({ ))} diff --git a/packages/app/features/wallet/WalletScreen.tsx b/packages/app/features/wallet/WalletScreen.tsx index 24b9b05e..6ce0292c 100644 --- a/packages/app/features/wallet/WalletScreen.tsx +++ b/packages/app/features/wallet/WalletScreen.tsx @@ -144,7 +144,7 @@ export function WalletScreen() { })} - + Credentials diff --git a/packages/app/utils/formatCredentialSubject.ts b/packages/app/utils/formatCredentialSubject.ts index c6e0760d..aea57c50 100644 --- a/packages/app/utils/formatCredentialSubject.ts +++ b/packages/app/utils/formatCredentialSubject.ts @@ -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 @@ -37,21 +56,40 @@ 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, - 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, + depth + 1, + title, + sanitizeString(key) + ) ) - ) + } } }) @@ -59,5 +97,53 @@ export function formatCredentialSubject( { 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) + }), + } + }) } diff --git a/packages/ui/src/table/TableRow.tsx b/packages/ui/src/table/TableRow.tsx index efd07fca..42813cbc 100644 --- a/packages/ui/src/table/TableRow.tsx +++ b/packages/ui/src/table/TableRow.tsx @@ -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 ? : undefined return ( - - {attribute} - - - {value} - + + + + {attribute} + + {value && {value}} + {/* Render image on the left if no value */} + {!value && renderedImage} + + {/* Otherwise render image on the right */} + {value && renderedImage} + ) }