diff --git a/apps/easypid/src/app/(app)/_layout.tsx b/apps/easypid/src/app/(app)/_layout.tsx index ac4fd2d5..9bfc7ca8 100644 --- a/apps/easypid/src/app/(app)/_layout.tsx +++ b/apps/easypid/src/app/(app)/_layout.tsx @@ -1,4 +1,4 @@ -import { Redirect, Stack, useGlobalSearchParams, useLocalSearchParams, usePathname, useRouter } from 'expo-router' +import { Redirect, Stack, useGlobalSearchParams, usePathname, useRouter } from 'expo-router' import { TypedArrayEncoder } from '@credo-ts/core' import { useSecureUnlock } from '@easypid/agent' @@ -135,7 +135,7 @@ export default function AppLayout() { - + diff --git a/apps/easypid/src/app/(app)/federation.tsx b/apps/easypid/src/app/(app)/federation.tsx new file mode 100644 index 00000000..70a8a0db --- /dev/null +++ b/apps/easypid/src/app/(app)/federation.tsx @@ -0,0 +1,18 @@ +import { FunkeFederationDetailScreen } from '@easypid/features/wallet/FunkeFederationDetailScreen' +import type { TrustedEntity } from '@package/agent' +import { useLocalSearchParams } from 'expo-router' + +export default function Screen() { + const { entityId, trustedEntities, name, logo } = useLocalSearchParams() + + const trustedEntitiesArray = JSON.parse(decodeURIComponent(trustedEntities as string)) as Array + + return ( + + ) +} diff --git a/apps/easypid/src/app/(app)/issuer.tsx b/apps/easypid/src/app/(app)/issuer.tsx deleted file mode 100644 index 12e186cf..00000000 --- a/apps/easypid/src/app/(app)/issuer.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { FunkeIssuerDetailScreen } from '@easypid/features/wallet/FunkeIssuerDetailScreen' -import { useLocalSearchParams } from 'expo-router' - -export default function Screen() { - const { host } = useLocalSearchParams() - - return -} diff --git a/apps/easypid/src/features/receive/FunkeCredentialNotificationScreen.tsx b/apps/easypid/src/features/receive/FunkeCredentialNotificationScreen.tsx index e7194bf8..9107e014 100644 --- a/apps/easypid/src/features/receive/FunkeCredentialNotificationScreen.tsx +++ b/apps/easypid/src/features/receive/FunkeCredentialNotificationScreen.tsx @@ -352,7 +352,6 @@ export function FunkeCredentialNotificationScreen() { logo={credentialDisplay.issuer.logo} entityId={issuerMetadata?.credential_issuer as string} lastInteractionDate={activities[0]?.date} - approvalsCount={0} onContinue={onCheckCardContinue} /> ), diff --git a/apps/easypid/src/features/receive/slides/VerifyPartySlide.tsx b/apps/easypid/src/features/receive/slides/VerifyPartySlide.tsx index 7bc8909b..538d8096 100644 --- a/apps/easypid/src/features/receive/slides/VerifyPartySlide.tsx +++ b/apps/easypid/src/features/receive/slides/VerifyPartySlide.tsx @@ -1,4 +1,4 @@ -import type { DisplayImage } from '@package/agent' +import type { DisplayImage, TrustedEntity } from '@package/agent' import { Circle, @@ -25,8 +25,8 @@ interface VerifyPartySlideProps { logo?: DisplayImage backgroundColor?: string lastInteractionDate?: string - approvalsCount?: number onContinue?: () => Promise + trustedEntities?: Array } export const VerifyPartySlide = ({ @@ -36,8 +36,8 @@ export const VerifyPartySlide = ({ logo, backgroundColor, lastInteractionDate, - approvalsCount, onContinue, + trustedEntities, }: VerifyPartySlideProps) => { const router = useRouter() const { onNext, onCancel } = useWizard() @@ -54,7 +54,9 @@ export const VerifyPartySlide = ({ } const onPressVerifiedIssuer = withHaptics(() => { - router.push(`/issuer?entityId=${entityId}`) + router.push( + `/federation?name=${encodeURIComponent(name ?? '')}&logo=${encodeURIComponent(logo?.url ?? '')}&entityId=${encodeURIComponent(entityId)}&trustedEntities=${encodeURIComponent(JSON.stringify(trustedEntities ?? []))}` + ) }) const onPressInteraction = withHaptics(() => { @@ -93,15 +95,20 @@ export const VerifyPartySlide = ({ - {approvalsCount ? ( + {trustedEntities && trustedEntities.length > 0 ? ( ) : ( - + )} credentialsForRequest && getOpenIdFedIssuerMetadata(credentialsForRequest.verifier.entityId), - [credentialsForRequest] - ) const lastInteractionDate = activities?.[0]?.date + const shouldUsePin = useShouldUsePinForSubmission(credentialsForRequest) useEffect(() => { if (credentialsForRequest) return @@ -154,8 +146,8 @@ export function FunkeOpenIdPresentationNotificationScreen() { entityId={credentialsForRequest?.verifier.entityId as string} verifierName={credentialsForRequest?.verifier.name} logo={credentialsForRequest?.verifier.logo} + trustedEntities={credentialsForRequest?.verifier.trustedEntities} lastInteractionDate={lastInteractionDate} - approvalsCount={fedDisplayData?.approvals.length} onComplete={() => pushToWallet('replace')} /> ) diff --git a/apps/easypid/src/features/share/FunkePresentationNotificationScreen.tsx b/apps/easypid/src/features/share/FunkePresentationNotificationScreen.tsx index 0e6010e8..eeea2449 100644 --- a/apps/easypid/src/features/share/FunkePresentationNotificationScreen.tsx +++ b/apps/easypid/src/features/share/FunkePresentationNotificationScreen.tsx @@ -1,4 +1,4 @@ -import type { DisplayImage, FormattedSubmission } from '@package/agent' +import type { DisplayImage, FormattedSubmission, TrustedEntity } from '@package/agent' import { type SlideStep, SlideWizard } from '@package/app' import { LoadingRequestSlide } from '../receive/slides/LoadingRequestSlide' @@ -13,8 +13,7 @@ interface FunkePresentationNotificationScreenProps { verifierName?: string logo?: DisplayImage lastInteractionDate?: string - approvalsCount?: number - + trustedEntities?: Array submission?: FormattedSubmission usePin: boolean isAccepting: boolean @@ -28,13 +27,13 @@ export function FunkePresentationNotificationScreen({ verifierName, logo, lastInteractionDate, - approvalsCount, usePin, onAccept, onDecline, isAccepting, submission, onComplete, + trustedEntities, }: FunkePresentationNotificationScreenProps) { return ( ), }, diff --git a/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx b/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx new file mode 100644 index 00000000..60396b4d --- /dev/null +++ b/apps/easypid/src/features/wallet/FunkeFederationDetailScreen.tsx @@ -0,0 +1,104 @@ +import type { TrustedEntity } from '@package/agent' +import { + Circle, + FlexPage, + Heading, + HeroIcons, + IconContainer, + Image, + MessageBox, + Paragraph, + ScrollView, + type ScrollViewRefType, + XStack, + YStack, +} from '@package/ui' +import { TextBackButton, useScrollViewPosition } from 'packages/app/src' +import React, { useRef } from 'react' +import { useSafeAreaInsets } from 'react-native-safe-area-context' + +interface FunkeFederationDetailScreenProps { + name: string + logo?: string + entityId?: string + trustedEntities?: Array +} + +export function FunkeFederationDetailScreen({ + name, + logo, + entityId, + trustedEntities = [], +}: FunkeFederationDetailScreenProps) { + const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition() + const { bottom } = useSafeAreaInsets() + const scrollViewRef = useRef(null) + + return ( + + + + + About this party + } + /> + + {logo ? ( + + + + ) : ( + + + + )} + {name} + + + + Trusted by + + {trustedEntities.length > 0 ? ( + <>A list of organizations and whether they have approved {name}. + ) : ( + <>There are no organizations that have approved {name}. + )} + + + + {trustedEntities.map((entity) => { + return ( + + {entity.logo_uri && ( + + + + )} + + + {entity.organization_name} + + } /> + + + ) + })} + + + + + + + + + ) +} diff --git a/apps/easypid/src/features/wallet/FunkeIssuerDetailScreen.tsx b/apps/easypid/src/features/wallet/FunkeIssuerDetailScreen.tsx deleted file mode 100644 index 95ed1a50..00000000 --- a/apps/easypid/src/features/wallet/FunkeIssuerDetailScreen.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { getOpenIdFedIssuerMetadata } from '@easypid/utils/issuer' -import { - Circle, - FlexPage, - Heading, - HeroIcons, - Image, - MessageBox, - Paragraph, - ScrollView, - type ScrollViewRefType, - Stack, - XStack, - YStack, - useToastController, -} from '@package/ui' -import { useRouter } from 'expo-router' -import { TextBackButton, useHaptics, useScrollViewPosition } from 'packages/app/src' -import { useRef } from 'react' -import { Linking } from 'react-native' -import { useSafeAreaInsets } from 'react-native-safe-area-context' - -interface FunkeIssuerDetailScreenProps { - host: string -} - -export function FunkeIssuerDetailScreen({ host }: FunkeIssuerDetailScreenProps) { - const toast = useToastController() - const router = useRouter() - const { withHaptics, errorHaptic } = useHaptics() - const data = getOpenIdFedIssuerMetadata(host) - - if (!data) { - router.back() - errorHaptic() - return toast.show('Currently unavailable.', { - customData: { - preset: 'warning', - }, - }) - } - - const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition() - const { bottom } = useSafeAreaInsets() - const scrollViewRef = useRef(null) - - const openDomain = withHaptics(() => { - Linking.openURL(`https://${host}`) - }) - - return ( - - - - - About this party - } - /> - - - - - - {data.display.name} - - {host} - - - - - - Approvals - A list of entities that have approved {data.display.name}. - - - {data.approvals.map((approval) => ( - - - - - - {approval.name} - Owned by {approval.ownedBy.display.name} - - - ))} - - - - - Trust marks - Certifications that verify {data.display.name}'s security and quality standards. - - - {data.certifications.map((certification) => ( - - {certification} - - ))} - - - - - - - - - ) -} diff --git a/apps/easypid/src/utils/issuer.ts b/apps/easypid/src/utils/issuer.ts deleted file mode 100644 index 15d8ac69..00000000 --- a/apps/easypid/src/utils/issuer.ts +++ /dev/null @@ -1,52 +0,0 @@ -const DEFAULT_FUNKE_HOST = 'funke.animo.id' - -const FUNKE_ISSUER_DATA = { - host: DEFAULT_FUNKE_HOST, - display: { - name: 'Animo', - logo: { - url: 'https://i.imgur.com/5kxpKzA.png', - altText: 'Animo Solutions logo', - }, - }, - approvals: [ - { - id: '7c82e94d-d650-4230-9586-cc5e0bec1d88', - name: 'Dutch Gov eID list', - ownedBy: { - did: 'did:web:dutch.gov', - display: { - name: 'Dutch Gov', - logo: { - url: 'https://i.imgur.com/lqfQV5g.png', - altText: 'Dutch Gov logo', - }, - isGovernment: true, - }, - }, - }, - { - id: '1e5b0c77-891f-4db6-b5b0-b174e7b38d30', - name: 'Registered Businesses', - ownedBy: { - did: 'did:web:registered.businesses', - display: { - name: 'Registered Businesses', - logo: { - url: 'https://i.imgur.com/eoXTZS5.jpeg', - altText: 'Registered Businesses logo', - }, - }, - }, - }, - ], - certifications: ['eIDAS compliant', 'ISO/IEC 27001'], -} - -export const getOpenIdFedIssuerMetadata = (host: string) => { - if (host === FUNKE_ISSUER_DATA.host) { - return FUNKE_ISSUER_DATA - } - - return null -} diff --git a/packages/agent/src/invitation/handler.ts b/packages/agent/src/invitation/handler.ts index 584f882c..0944cd6e 100644 --- a/packages/agent/src/invitation/handler.ts +++ b/packages/agent/src/invitation/handler.ts @@ -55,6 +55,8 @@ import { getCredentialBindingResolver } from '../openid4vc/credentialBindingReso import { extractOpenId4VcCredentialMetadata, setOpenId4VcCredentialMetadata } from '../openid4vc/displayMetadata' import { BiometricAuthenticationError } from './error' import { fetchInvitationDataUrl } from './fetchInvitation' +import { TRUSTED_ENTITIES } from './trustedEntities' +import type { TrustedEntity } from './trustedEntities' export async function resolveOpenId4VciOffer({ agent, @@ -448,20 +450,21 @@ export async function withTrustedCertificate( } export type CredentialsForProofRequest = Awaited> -export const getCredentialsForProofRequest = async ({ - agent, - data, - uri, - allowUntrustedFederation = true, // TODO: True for now -}: { + +export type GetCredentialsForProofRequestOptions = { agent: EitherAgent - // Either data or uri can be provided data?: string uri?: string allowUntrustedFederation?: boolean -}) => { - let requestUri: string +} +export const getCredentialsForProofRequest = async ({ + agent, + data, + uri, + allowUntrustedFederation = true, +}: GetCredentialsForProofRequestOptions) => { + let requestUri: string let requestData = data const { entityId = undefined, data: fromFederationData = null } = allowUntrustedFederation @@ -495,6 +498,23 @@ export const getCredentialsForProofRequest = async ({ resolved.authorizationRequest.authorizationRequestPayload.client_metadata } + let trustedEntities: Array = [] + if (entityId) { + const resolvedChains = await agent.modules.openId4VcHolder.resolveOpenIdFederationChains({ + entityId: entityId, + trustAnchorEntityIds: TRUSTED_ENTITIES, + }) + + trustedEntities = resolvedChains + .map((chain) => ({ + entity_id: chain.trustAnchorEntityConfiguration.sub, + organization_name: + chain.trustAnchorEntityConfiguration.metadata?.federation_entity?.organization_name ?? 'Unknown entity', + logo_uri: chain.trustAnchorEntityConfiguration.metadata?.federation_entity?.logo_uri, + })) + .filter((entity, index, self) => self.findIndex((e) => e.entity_id === entity.entity_id) === index) + } + let formattedSubmission: FormattedSubmission if (resolved.presentationExchange) { formattedSubmission = formatDifPexCredentialsForRequest( @@ -530,6 +550,7 @@ export const getCredentialsForProofRequest = async ({ } : undefined, name: clientMetadata?.client_name, + trustedEntities, }, formattedSubmission, } as const diff --git a/packages/agent/src/invitation/index.ts b/packages/agent/src/invitation/index.ts index 35c52ff0..f5b5f7bb 100644 --- a/packages/agent/src/invitation/index.ts +++ b/packages/agent/src/invitation/index.ts @@ -40,3 +40,5 @@ export { } from './handler' export { shareProof } from './shareProof' export * from './error' + +export type { TrustedEntity } from './trustedEntities' diff --git a/packages/agent/src/invitation/trustedEntities.ts b/packages/agent/src/invitation/trustedEntities.ts new file mode 100644 index 00000000..0cd3b226 --- /dev/null +++ b/packages/agent/src/invitation/trustedEntities.ts @@ -0,0 +1,15 @@ +const BASE_URL = 'https://funke.animo.id/siop' + +export const TRUSTED_ENTITIES = [ + `${BASE_URL}/0193687b-0c27-7b82-a686-ff857dc6bbb3`, + `${BASE_URL}/0193687f-20d8-720a-9139-ed939ba510fa`, + `${BASE_URL}/019368ed-3787-7669-b7f4-8c012238e90d`, + `${BASE_URL}/01936907-56a3-7007-a61f-44bff8b5d175`, + `${BASE_URL}/01936903-8879-733f-8eaf-6f2fa862099c`, +] satisfies [string, ...string[]] + +export type TrustedEntity = { + entity_id: string + organization_name: string + logo_uri?: string +} diff --git a/packages/ui/src/components/InfoButton.tsx b/packages/ui/src/components/InfoButton.tsx index 08a6c8da..caa9932d 100644 --- a/packages/ui/src/components/InfoButton.tsx +++ b/packages/ui/src/components/InfoButton.tsx @@ -21,6 +21,10 @@ const infoButtonVariants = { icon: , accent: '$danger-500', }, + info: { + icon: , + accent: '$grey-500', + }, // States expired: { @@ -95,7 +99,7 @@ export function InfoButton({ )} - + {title}