diff --git a/apps/easypid/src/features/share/components/RequestedAttributesSection.tsx b/apps/easypid/src/features/share/components/RequestedAttributesSection.tsx index b32067fd..d914d848 100644 --- a/apps/easypid/src/features/share/components/RequestedAttributesSection.tsx +++ b/apps/easypid/src/features/share/components/RequestedAttributesSection.tsx @@ -4,29 +4,70 @@ import { type FormattedSubmissionEntrySatisfied, getDisclosedAttributeNamesForDisplay, getUnsatisfiedAttributePathsForDisplay, + useCredentialsForDisplay, } from '@package/agent' import { CardWithAttributes } from '@package/app' -import { Heading, Paragraph, YStack } from '@package/ui' +import { Heading, HeroIcons, Paragraph, XStack, YStack } from '@package/ui' +import { useEffect, useMemo, useState } from 'react' export type RequestedAttributesSectionProps = { submission: FormattedSubmission } +const copy = { + satisfied: { + title: 'REQUESTED CARDS', + description: 'The following cards will be shared.', + variant: 'default', + }, + unsatisfied: { + title: 'CARDS UNAVAILABLE', + description: "You don't have the requested card(s).", + variant: '$danger-500', + }, + invalid: { + title: 'ATTRIBUTES UNAVAILABLE', + description: 'The verifier requested attributes that are not present in your card(s).', + variant: '$danger-500', + }, +} + export function RequestedAttributesSection({ submission }: RequestedAttributesSectionProps) { const satisfiedEntries = submission.entries.filter((e): e is FormattedSubmissionEntrySatisfied => e.isSatisfied) const unsatisfiedEntries = submission.entries.filter((e): e is FormattedSubmissionEntryNotSatisfied => !e.isSatisfied) + const { credentials } = useCredentialsForDisplay() + const [state, setState] = useState<'satisfied' | 'unsatisfied' | 'invalid'>( + satisfiedEntries.length === 0 ? 'satisfied' : 'unsatisfied' + ) + + useEffect(() => { + const hasInvalidCredential = unsatisfiedEntries.some((entry) => + credentials.find((c) => c.metadata.type === `https://${entry.name}`) + ) + + if (hasInvalidCredential) setState('invalid') + }, [credentials, unsatisfiedEntries]) + + const formatUnsatisfiedEntries = useMemo(() => { + return unsatisfiedEntries.map((entry) => { + const credential = credentials.find((c) => c.metadata.type === `https://${entry.name}`) + return { + ...entry, + credential, + } + }) + }, [credentials, unsatisfiedEntries]) return ( - {satisfiedEntries.length > 0 ? 'REQUESTED CARDS' : 'UNAVAILABLE CARDS'} - - {unsatisfiedEntries.length === 0 - ? 'The following cards will be shared.' - : satisfiedEntries.length === 0 - ? `You don't have the requested card(s).` - : `You don't have all of the requested cards.`} - + + {state !== 'satisfied' && } + + {copy[state].title} + + + {copy[state].description} {/* We always take the first one for now (no selection) */} {satisfiedEntries.map(({ credentials: [credential], ...entry }) => { @@ -54,26 +95,47 @@ export function RequestedAttributesSection({ submission }: RequestedAttributesSe /> ) })} - {unsatisfiedEntries.length > 0 && ( + {formatUnsatisfiedEntries.length > 0 && ( <> - {satisfiedEntries.length !== 0 && ( - - UNAVAILABLE CARDS - - )} - {unsatisfiedEntries.map((entry) => ( - - ))} + {formatUnsatisfiedEntries.map(({ credential, ...entry }) => { + const availableAttributes = Object.keys(credential?.attributes ?? {}) + const requestedAttributes = getUnsatisfiedAttributePathsForDisplay(entry.requestedAttributePaths) + const missingAttributes = requestedAttributes.filter((attr) => !availableAttributes.includes(attr)) + const attributeValuesThatCouldBeDisclosed = requestedAttributes.filter((attr) => + availableAttributes.includes(attr) + ) + + // Attributes with their values that could be found in the credential + const attributesWithValuesThatCouldBeDisclosed = attributeValuesThatCouldBeDisclosed.reduce< + Record + >( + (acc, attr) => ({ + ...acc, + [attr]: credential?.attributes[attr], + }), + {} + ) + + // Add missing attributes to the disclosed payload without values + const disclosedPayloadWithMissingAttributes = { + ...attributesWithValuesThatCouldBeDisclosed, + ...Object.fromEntries(missingAttributes.map((attr) => [attr, 'value-not-found'])), + } + + return ( + + ) + })} )} diff --git a/apps/easypid/src/features/share/slides/ShareCredentialsSlide.tsx b/apps/easypid/src/features/share/slides/ShareCredentialsSlide.tsx index 5db1436c..26a30cae 100644 --- a/apps/easypid/src/features/share/slides/ShareCredentialsSlide.tsx +++ b/apps/easypid/src/features/share/slides/ShareCredentialsSlide.tsx @@ -2,7 +2,7 @@ import type { OverAskingResponse } from '@easypid/use-cases/OverAskingApi' import type { DisplayImage, FormattedSubmission } from '@package/agent' import { DualResponseButtons, useScrollViewPosition } from '@package/app' import { useWizard } from '@package/app' -import { Button, Heading, HeroIcons, MessageBox, Paragraph, ScrollView, YStack } from '@package/ui' +import { Button, Heading, HeroIcons, MessageBox, ScrollView, YStack } from '@package/ui' import { useState } from 'react' import { Spacer } from 'tamagui' import { RequestPurposeSection } from '../components/RequestPurposeSection' @@ -104,12 +104,7 @@ export const ShareCredentialsSlide = ({ isLoading={isProcessing} /> ) : ( - - - You don't have the required cards - - Close - + Close )} diff --git a/packages/app/src/components/CardWithAttributes.tsx b/packages/app/src/components/CardWithAttributes.tsx index 8d23f73b..f42caa0b 100644 --- a/packages/app/src/components/CardWithAttributes.tsx +++ b/packages/app/src/components/CardWithAttributes.tsx @@ -16,7 +16,7 @@ import { useRouter } from 'expo-router' import { useMemo } from 'react' import { BlurBadge } from './BlurBadge' -interface CardWithAttributesProps { +export interface CardWithAttributesProps { id?: string name: string backgroundColor?: string diff --git a/packages/app/src/components/CredentialAttributes.tsx b/packages/app/src/components/CredentialAttributes.tsx index 080e4f49..30ad8513 100644 --- a/packages/app/src/components/CredentialAttributes.tsx +++ b/packages/app/src/components/CredentialAttributes.tsx @@ -229,6 +229,8 @@ const PrimitiveArrayRow = ({ name, value }: { name: string; value: (string | num } const ValueRow = ({ name, value }: { name: string; value: string }) => { + const isInvalid = value === 'value-not-found' + return ( { {name} - {value} + {isInvalid ? 'Not found' : value} ) }