diff --git a/apps/easypid/src/features/share/FunkeOpenIdPresentationNotificationScreen.tsx b/apps/easypid/src/features/share/FunkeOpenIdPresentationNotificationScreen.tsx index fe7f26bc..a0b02017 100644 --- a/apps/easypid/src/features/share/FunkeOpenIdPresentationNotificationScreen.tsx +++ b/apps/easypid/src/features/share/FunkeOpenIdPresentationNotificationScreen.tsx @@ -1,14 +1,18 @@ import { BiometricAuthenticationCancelledError, type CredentialsForProofRequest, + type FormattedSubmissionEntrySatisfied, getCredentialsForProofRequest, + getDisclosedAttributeNamesForDisplay, shareProof, } from '@package/agent' import { useToastController } from '@package/ui' import { useLocalSearchParams } from 'expo-router' -import React, { useEffect, useState, useMemo, useCallback } from 'react' +import React, { useEffect, useState, useCallback } from 'react' import { useAppAgent } from '@easypid/agent' +import { analyzeVerification } from '@easypid/use-cases/ValidateVerification' +import type { VerificationAnalysisResponse } from '@easypid/use-cases/ValidateVerification' import { usePushToWallet } from '@package/app/src/hooks/usePushToWallet' import { setWalletServiceProviderPin } from '../../crypto/WalletServiceProviderClient' import { useShouldUsePinForSubmission } from '../../hooks/useShouldUsePinForPresentation' @@ -53,6 +57,40 @@ export function FunkeOpenIdPresentationNotificationScreen() { }) }, [credentialsForRequest, params.data, params.uri, toast.show, agent, pushToWallet, toast]) + const [verificationAnalysis, setVerificationAnalysis] = useState<{ + isLoading: boolean + result: VerificationAnalysisResponse | undefined + }>({ + isLoading: false, + result: undefined, + }) + + useEffect(() => { + if (!credentialsForRequest?.formattedSubmission || !credentialsForRequest?.formattedSubmission.areAllSatisfied) { + return + } + setVerificationAnalysis((prev) => ({ ...prev, isLoading: true })) + + const submission = credentialsForRequest.formattedSubmission + const requestedCards = submission.entries + .filter((entry): entry is FormattedSubmissionEntrySatisfied => entry.isSatisfied) + .flatMap((entry) => entry.credentials) + + analyzeVerification({ + verifier: { + name: credentialsForRequest.verifier.name ?? 'No name provided', + domain: credentialsForRequest.verifier.hostName ?? 'No domain provided', + }, + name: submission.name ?? 'No name provided', + purpose: submission.purpose ?? 'No purpose provided', + cards: requestedCards.map((credential) => ({ + name: credential.credential.display.name ?? 'Card name', + subtitle: credential.credential.display.description ?? 'Card description', + requestedAttributes: getDisclosedAttributeNamesForDisplay(credential), + })), + }).then((result) => setVerificationAnalysis((prev) => ({ ...prev, isLoading: false, result }))) + }, [credentialsForRequest]) + const onProofAccept = useCallback( async (pin?: string): Promise => { if (!credentialsForRequest) @@ -149,6 +187,7 @@ export function FunkeOpenIdPresentationNotificationScreen() { trustedEntities={credentialsForRequest?.verifier.trustedEntities} lastInteractionDate={lastInteractionDate} onComplete={() => pushToWallet('replace')} + verificationAnalysis={verificationAnalysis} /> ) } diff --git a/apps/easypid/src/features/share/FunkePresentationNotificationScreen.tsx b/apps/easypid/src/features/share/FunkePresentationNotificationScreen.tsx index eeea2449..c41b773e 100644 --- a/apps/easypid/src/features/share/FunkePresentationNotificationScreen.tsx +++ b/apps/easypid/src/features/share/FunkePresentationNotificationScreen.tsx @@ -1,5 +1,6 @@ import type { DisplayImage, FormattedSubmission, TrustedEntity } from '@package/agent' +import type { VerificationAnalysisResult } from '@easypid/use-cases/ValidateVerification' import { type SlideStep, SlideWizard } from '@package/app' import { LoadingRequestSlide } from '../receive/slides/LoadingRequestSlide' import { VerifyPartySlide } from '../receive/slides/VerifyPartySlide' @@ -13,6 +14,7 @@ interface FunkePresentationNotificationScreenProps { verifierName?: string logo?: DisplayImage lastInteractionDate?: string + verificationAnalysis: VerificationAnalysisResult trustedEntities?: Array submission?: FormattedSubmission usePin: boolean @@ -33,6 +35,7 @@ export function FunkePresentationNotificationScreen({ isAccepting, submission, onComplete, + verificationAnalysis, trustedEntities, }: FunkePresentationNotificationScreenProps) { return ( @@ -71,6 +74,7 @@ export function FunkePresentationNotificationScreen({ logo={logo} submission={submission} isAccepting={isAccepting} + verificationAnalysis={verificationAnalysis} /> ), }, diff --git a/apps/easypid/src/features/share/components/RequestPurposeSection.tsx b/apps/easypid/src/features/share/components/RequestPurposeSection.tsx index d533676d..1ebd2f24 100644 --- a/apps/easypid/src/features/share/components/RequestPurposeSection.tsx +++ b/apps/easypid/src/features/share/components/RequestPurposeSection.tsx @@ -1,30 +1,87 @@ -import { Circle, Heading, HeroIcons, Image, MessageBox, Stack, YStack } from '@package/ui' +import type { VerificationAnalysisResult } from '@easypid/use-cases/ValidateVerification' +import { + AnimatedStack, + Circle, + Heading, + HeroIcons, + Image, + InfoSheet, + MessageBox, + Stack, + XStack, + YStack, + useScaleAnimation, +} from '@package/ui' import type { DisplayImage } from 'packages/agent/src' +import { useState } from 'react' +import { ZoomIn } from 'react-native-reanimated' +import { VerificationAnalysisIcon } from './VerificationAnalysisIcon' interface RequestPurposeSectionProps { purpose: string logo?: DisplayImage + verificationAnalysis?: VerificationAnalysisResult } -export function RequestPurposeSection({ purpose, logo }: RequestPurposeSectionProps) { +export function RequestPurposeSection({ purpose, logo, verificationAnalysis }: RequestPurposeSectionProps) { + const [isAnalysisModalOpen, setIsAnalysisModalOpen] = useState(false) + + const { handlePressIn, handlePressOut, pressStyle } = useScaleAnimation() + + const toggleAnalysisModal = () => setIsAnalysisModalOpen(!isAnalysisModalOpen) + return ( - - PURPOSE - - {logo?.url ? ( - {logo.altText} - ) : ( - - - + <> + + {verificationAnalysis?.result?.validRequest === 'no' && ( + + } + variant="error" + message="The purpose given does not match the data requested." + /> + + )} + + PURPOSE + + {verificationAnalysis && ( + + + )} - - } + + + + {logo?.url ? ( + {logo.altText} + ) : ( + + + + )} + + } + /> + + - + ) } diff --git a/apps/easypid/src/features/share/components/VerificationAnalysisIcon.tsx b/apps/easypid/src/features/share/components/VerificationAnalysisIcon.tsx new file mode 100644 index 00000000..87806ec4 --- /dev/null +++ b/apps/easypid/src/features/share/components/VerificationAnalysisIcon.tsx @@ -0,0 +1,21 @@ +import type { VerificationAnalysisResult } from '@easypid/use-cases/ValidateVerification' +import { HeroIcons, Spinner } from 'packages/ui/src' + +interface VerificationAnalysisIconProps { + verificationAnalysis: VerificationAnalysisResult +} + +export function VerificationAnalysisIcon({ verificationAnalysis }: VerificationAnalysisIconProps) { + if (!verificationAnalysis.result || verificationAnalysis.isLoading) return + + if (verificationAnalysis.result.validRequest === 'could_not_determine') { + // AI doesn't know or an error was thrown + return null + } + + if (verificationAnalysis.result.validRequest === 'yes') { + return + } + + return +} diff --git a/apps/easypid/src/features/share/slides/ShareCredentialsSlide.tsx b/apps/easypid/src/features/share/slides/ShareCredentialsSlide.tsx index 70fb545a..4fb48581 100644 --- a/apps/easypid/src/features/share/slides/ShareCredentialsSlide.tsx +++ b/apps/easypid/src/features/share/slides/ShareCredentialsSlide.tsx @@ -1,3 +1,4 @@ +import type { VerificationAnalysisResult } from '@easypid/use-cases/ValidateVerification' import type { DisplayImage, FormattedSubmission } from '@package/agent' import { DualResponseButtons, usePushToWallet, useScrollViewPosition } from '@package/app' import { useWizard } from '@package/app' @@ -16,6 +17,7 @@ interface ShareCredentialsSlideProps { isAccepting: boolean isOffline?: boolean + verificationAnalysis?: VerificationAnalysisResult } export const ShareCredentialsSlide = ({ @@ -25,6 +27,7 @@ export const ShareCredentialsSlide = ({ onDecline, isAccepting, isOffline, + verificationAnalysis, }: ShareCredentialsSlideProps) => { const { onNext, onCancel } = useWizard() const [scrollViewHeight, setScrollViewHeight] = useState(0) @@ -85,6 +88,7 @@ export const ShareCredentialsSlide = ({ purpose={ submission.purpose ?? 'No information was provided on the purpose of the data request. Be cautious' } + verificationAnalysis={verificationAnalysis} logo={logo} /> )} diff --git a/apps/easypid/src/use-cases/ValidateVerification.ts b/apps/easypid/src/use-cases/ValidateVerification.ts new file mode 100644 index 00000000..2431a681 --- /dev/null +++ b/apps/easypid/src/use-cases/ValidateVerification.ts @@ -0,0 +1,54 @@ +const PLAYGROUND_URL = 'https://funke.animo.id' + +export type VerificationAnalysisInput = { + verifier: { + name: string + domain: string + } + name: string + purpose: string + cards: Array<{ + name: string + subtitle: string + requestedAttributes: Array + }> +} + +export type VerificationAnalysisResponse = { + validRequest: 'yes' | 'no' | 'could_not_determine' + reason: string +} + +export type VerificationAnalysisResult = { + isLoading: boolean + result: VerificationAnalysisResponse | undefined +} + +export const analyzeVerification = async ({ + verifier, + name, + purpose, + cards, +}: VerificationAnalysisInput): Promise => { + try { + const response = await fetch(`${PLAYGROUND_URL}/api/validate-verification-request`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ verifier, name, purpose, cards }), + }) + + if (!response.ok) { + throw new Error(`Request to AI returned ${response.status}`) + } + + return await response.json() + } catch (error) { + console.error('AI analysis failed:', error) + return { + validRequest: 'could_not_determine', + reason: 'An error occurred while validating the verification', + } + } +} diff --git a/packages/agent/src/format/formatPresentation.ts b/packages/agent/src/format/formatPresentation.ts index 26fe7192..12f64ba6 100644 --- a/packages/agent/src/format/formatPresentation.ts +++ b/packages/agent/src/format/formatPresentation.ts @@ -14,7 +14,6 @@ import { getCredentialForDisplay, getDisclosedAttributePathArrays, } from '../display' -import { applyLimitdisclosureForSdJwtRequestedPayload } from './disclosureFrame' export interface FormattedSubmission { name?: string diff --git a/packages/ui/src/panels/InfoSheet.tsx b/packages/ui/src/panels/InfoSheet.tsx index 95104f8c..b3ab2293 100644 --- a/packages/ui/src/panels/InfoSheet.tsx +++ b/packages/ui/src/panels/InfoSheet.tsx @@ -27,7 +27,7 @@ const infoSheetVariants = { background: '$warning-300', }, danger: { - icon: , + icon: , accent: '$danger-500', layer: '$danger-400', background: '$danger-300', diff --git a/packages/ui/src/panels/MessageBox.tsx b/packages/ui/src/panels/MessageBox.tsx index 601bce22..24741d1c 100644 --- a/packages/ui/src/panels/MessageBox.tsx +++ b/packages/ui/src/panels/MessageBox.tsx @@ -23,7 +23,7 @@ const messageBoxVariants = { color: '$white', }, success: { - bg: '$success-500', + bg: '$positive-500', color: '$white', }, } @@ -40,7 +40,11 @@ export function MessageBox({ message, textVariant = 'normal', variant = 'default return ( - {title && {title}} + {title && ( + + {title} + + )} {message}