diff --git a/app/components/listItems/NotificationListItem.tsx b/app/components/listItems/NotificationListItem.tsx index c61ea2b49..38994fce7 100644 --- a/app/components/listItems/NotificationListItem.tsx +++ b/app/components/listItems/NotificationListItem.tsx @@ -7,6 +7,8 @@ import { declineProofRequest as declineProof, getProofRequestAgentMessage, } from '@adeya/ssi' +// eslint-disable-next-line import/no-extraneous-dependencies +import { V2RequestPresentationMessage } from '@credo-ts/core' import { useNavigation } from '@react-navigation/core' import { StackNavigationProp } from '@react-navigation/stack' import React, { useState, useEffect } from 'react' @@ -67,6 +69,7 @@ const NotificationListItem: React.FC = ({ notificatio const { ColorPallet, TextTheme } = useTheme() const { agent } = useAppAgent() const [declineModalVisible, setDeclineModalVisible] = useState(false) + const [notificationDetails, setNotificationDetails] = useState(null) const [details, setDetails] = useState({ type: InfoBoxType.Info, title: undefined, @@ -215,6 +218,7 @@ const NotificationListItem: React.FC = ({ notificatio case NotificationType.ProofRequest: { const proofId = (notification as ProofExchangeRecord).id getProofRequestAgentMessage(agent, proofId).then(message => { + setNotificationDetails(message) if (message instanceof V1RequestPresentationMessage && message.indyProofRequest) { resolve({ type: InfoBoxType.Info, @@ -222,6 +226,17 @@ const NotificationListItem: React.FC = ({ notificatio body: message.indyProofRequest.name, buttonTitle: undefined, }) + } else if ( + message instanceof V2RequestPresentationMessage && + message?.formats?.length > 0 && + message?.formats[0].format.includes('dif/presentation-exchange') + ) { + resolve({ + type: InfoBoxType.Info, + title: t('ProofRequest.NewProofRequest'), + body: message?.requestAttachments[0]?.data?.json?.presentation_definition?.name ?? 'Proof Request', + buttonTitle: undefined, + }) } else { //TODO:(jl) Should we have a default message or stick with an empty string? resolve({ @@ -277,10 +292,21 @@ const NotificationListItem: React.FC = ({ notificatio } } else { onPress = () => { - navigation.getParent()?.navigate(Stacks.NotificationStack, { - screen: Screens.ProofRequest, - params: { proofId: (notification as ProofExchangeRecord).id }, - }) + // Added this check to navigate to different screen if proof request is of presentation exchange format + if ( + notificationDetails?.formats?.length > 0 && + notificationDetails?.formats[0].format.includes('dif/presentation-exchange') + ) { + navigation.getParent()?.navigate(Stacks.NotificationStack, { + screen: Screens.ProofRequestW3C, + params: { proofId: (notification as ProofExchangeRecord).id }, + }) + } else { + navigation.getParent()?.navigate(Stacks.NotificationStack, { + screen: Screens.ProofRequest, + params: { proofId: (notification as ProofExchangeRecord).id }, + }) + } } } onClose = toggleDeclineModalVisible diff --git a/app/components/misc/CredentialCard10.tsx b/app/components/misc/CredentialCard10.tsx index 558ccfef3..39aff1f65 100644 --- a/app/components/misc/CredentialCard10.tsx +++ b/app/components/misc/CredentialCard10.tsx @@ -1,4 +1,4 @@ -import { CredentialExchangeRecord } from '@adeya/ssi' +import { CredentialExchangeRecord, useConnections } from '@adeya/ssi' import { LegacyBrandingOverlay } from '@hyperledger/aries-oca' import { CredentialOverlay } from '@hyperledger/aries-oca/build/legacy' import React, { useEffect, useState } from 'react' @@ -75,7 +75,8 @@ const CredentialCard10: React.FC = ({ credential, style = const { OCABundleResolver } = useConfiguration() const [overlay, setOverlay] = useState>({}) const [isRevoked, setIsRevoked] = useState(false) - const credentialConnectionLabel = getCredentialConnectionLabel(credential) + const { records } = useConnections() + const credentialConnectionLabel = getCredentialConnectionLabel(records, credential) const styles = StyleSheet.create({ container: { diff --git a/app/components/misc/CredentialCard11.tsx b/app/components/misc/CredentialCard11.tsx index 6d389357c..e115e3e91 100644 --- a/app/components/misc/CredentialCard11.tsx +++ b/app/components/misc/CredentialCard11.tsx @@ -1,4 +1,4 @@ -import { CredentialExchangeRecord } from '@adeya/ssi' +import { CredentialExchangeRecord, useConnections } from '@adeya/ssi' import { BrandingOverlay } from '@hyperledger/aries-oca' import { Attribute, CredentialOverlay, Predicate } from '@hyperledger/aries-oca/build/legacy' import startCase from 'lodash.startcase' @@ -91,7 +91,8 @@ const CredentialCard11: React.FC = ({ const { ColorPallet, TextTheme, ListItems } = useTheme() const { OCABundleResolver } = useConfiguration() const [isRevoked, setIsRevoked] = useState(credential?.revocationNotification !== undefined) - const credentialConnectionLabel = getCredentialConnectionLabel(credential, connectionLabel) + const { records } = useConnections() + const credentialConnectionLabel = getCredentialConnectionLabel(records, credential, connectionLabel) const [isProofRevoked, setIsProofRevoked] = useState( credential?.revocationNotification !== undefined && !!proof, ) diff --git a/app/localization/en/index.ts b/app/localization/en/index.ts index 65b01f60b..ca53e8547 100644 --- a/app/localization/en/index.ts +++ b/app/localization/en/index.ts @@ -566,6 +566,7 @@ const translation = { "Explore": "Explore", "OrganizationDetails": "Organization Details", "ProofChangeCredential": "Choose a credential", + "ProofChangeCredentialW3C": "Choose a W3C credential", "DataRetention": "Data retention", "Organization": "Explore", "OrganizationConnection": "Connection" diff --git a/app/navigators/ContactStack.tsx b/app/navigators/ContactStack.tsx index e0518907d..8c63d5d90 100644 --- a/app/navigators/ContactStack.tsx +++ b/app/navigators/ContactStack.tsx @@ -15,6 +15,7 @@ import Home from '../screens/Home' import ListContacts from '../screens/ListContacts' import ProofDetails from '../screens/ProofDetails' import ProofRequest from '../screens/ProofRequest' +import ProofRequestW3C from '../screens/ProofRequestW3C' import WhatAreContacts from '../screens/WhatAreContacts' import { ContactStackParams, Screens } from '../types/navigators' import { testIdWithKey } from '../utils/testable' @@ -81,6 +82,11 @@ const ContactStack: React.FC = () => { component={ProofRequest} options={{ title: t('Screens.ProofRequest') }} /> + { component={ProofRequest} options={{ title: t('Screens.ProofRequest') }} /> + { component={ProofRequest} options={{ title: t('Screens.ProofRequest') }} /> + { component={ProofChangeCredential} options={{ title: t('Screens.ProofChangeCredential') }} /> + = ({ navigation, route brandingOverlay: undefined, }) - const credentialConnectionLabel = getCredentialConnectionLabel(credential) + const { records } = useConnections() + const credentialConnectionLabel = getCredentialConnectionLabel(records, credential) const isPresentationFieldsEmpty = !overlay.brandingOverlay?.digest const styles = StyleSheet.create({ container: { diff --git a/app/screens/CredentialDetailsW3C.tsx b/app/screens/CredentialDetailsW3C.tsx index 7662a1658..66abc3dad 100644 --- a/app/screens/CredentialDetailsW3C.tsx +++ b/app/screens/CredentialDetailsW3C.tsx @@ -16,6 +16,7 @@ import { SafeAreaView } from 'react-native-safe-area-context' import Toast from 'react-native-toast-message' import CommonRemoveModal from '../components/modals/CommonRemoveModal' +import RecordRemove from '../components/record/RecordRemove' import W3CCredentialRecord from '../components/record/W3CCredentialRecord' import { ToastType } from '../components/toast/BaseToast' import { EventTypes } from '../constants' @@ -181,6 +182,11 @@ const CredentialDetailsW3C: React.FC = ({ navigation, ro setIsRemoveModalDisplayed(false) } + const handleOnRemove = () => { + setIsRemoveModalDisplayed(true) + } + + const callOnRemove = useCallback(() => handleOnRemove(), []) const callSubmitRemove = useCallback(() => handleSubmitRemove(), []) const callCancelRemove = useCallback(() => handleCancelRemove(), []) @@ -284,6 +290,7 @@ const CredentialDetailsW3C: React.FC = ({ navigation, ro {w3cCredential?.connectionLabel ?? ''} + ) } diff --git a/app/screens/CredentialOffer.tsx b/app/screens/CredentialOffer.tsx index be8459270..327d63091 100644 --- a/app/screens/CredentialOffer.tsx +++ b/app/screens/CredentialOffer.tsx @@ -9,6 +9,7 @@ import { declineCredentialOffer, sendCredentialProblemReport, AutoAcceptCredential, + useConnections, } from '@adeya/ssi' import { BrandingOverlay } from '@hyperledger/aries-oca' import { CredentialOverlay } from '@hyperledger/aries-oca/build/legacy' @@ -64,7 +65,8 @@ const CredentialOffer: React.FC = ({ navigation, route }) const credential = useCredentialById(credentialId) const [jsonLdOffer, setJsonLdOffer] = useState() const [tables, setTables] = useState([]) - const credentialConnectionLabel = getCredentialConnectionLabel(credential) + const { records } = useConnections() + const credentialConnectionLabel = getCredentialConnectionLabel(records, credential) const styles = StyleSheet.create({ headerTextContainer: { @@ -157,7 +159,10 @@ const CredentialOffer: React.FC = ({ navigation, route }) const credentialFormatData = await getFormattedCredentialData(agent, credential.id) // Added holder did as id if did is not present and negotiate offer - if (!credentialFormatData?.offer?.jsonld?.credential?.credentialSubject?.id) { + if ( + !credentialFormatData?.offer?.jsonld?.credential?.credentialSubject?.id && + credentialFormatData?.offer?.jsonld + ) { const holderDid = await getDefaultHolderDidDocument(agent) await agent.credentials.negotiateOffer({ credentialFormats: { @@ -218,31 +223,12 @@ const CredentialOffer: React.FC = ({ navigation, route }) {t('CredentialOffer.IsOfferingYouACredential')} - {!loading && credential && ( - - - - )} - - ) - } - - const jsonLdHeader = () => { - return ( - <> - - - - {credentialConnectionLabel || t('ContactDetails.AContact')}{' '} - {t('CredentialOffer.IsOfferingYouACredential')} - - {!loading && credential && ( )} @@ -291,7 +277,7 @@ const CredentialOffer: React.FC = ({ navigation, route }) tables={tables} fields={overlay.presentationFields || []} hideFieldValues={false} - header={jsonLdHeader} + header={header} footer={footer} /> ) : ( diff --git a/app/screens/ProofChangeCredentialW3C.tsx b/app/screens/ProofChangeCredentialW3C.tsx new file mode 100644 index 000000000..ea0fa297a --- /dev/null +++ b/app/screens/ProofChangeCredentialW3C.tsx @@ -0,0 +1,128 @@ +import { StackScreenProps } from '@react-navigation/stack' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { DeviceEventEmitter, FlatList, StyleSheet, Text, View } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' + +import RecordLoading from '../components/animated/RecordLoading' +import { CredentialCard } from '../components/misc' +import { EventTypes } from '../constants' +import { useTheme } from '../contexts/theme' +import { useAllCredentialsForProof } from '../hooks/proofs' +import { BifoldError } from '../types/error' +import { ProofRequestsStackParams, Screens } from '../types/navigators' +import { ProofCredentialItems } from '../types/proof-items' +import { testIdWithKey } from '../utils/testable' + +type ProofChangeProps = StackScreenProps + +const ProofChangeCredentialW3C: React.FC = ({ route, navigation }) => { + if (!route?.params) { + throw new Error('Change credential route params were not set properly') + } + const proofId = route.params.proofId + const selectedCred = route.params.selectedCred + const altCredentials = route.params.altCredentials + const onCredChange = route.params.onCredChange + const { ColorPallet, TextTheme } = useTheme() + const { t } = useTranslation() + const [loading, setLoading] = useState(false) + const [proofItems, setProofItems] = useState([]) + const credProofPromise = useAllCredentialsForProof(proofId) + const styles = StyleSheet.create({ + pageContainer: { + flex: 1, + }, + pageMargin: { + marginHorizontal: 20, + }, + cardLoading: { + backgroundColor: ColorPallet.brand.secondaryBackground, + flex: 1, + flexGrow: 1, + marginVertical: 35, + borderRadius: 15, + paddingHorizontal: 10, + }, + selectedCred: { + borderWidth: 5, + borderRadius: 15, + borderColor: ColorPallet.semantic.focus, + }, + }) + + useEffect(() => { + setLoading(true) + + credProofPromise + ?.then(value => { + if (value) { + const { groupedProof } = value + setLoading(false) + + const activeCreds = groupedProof.filter(proof => altCredentials.includes(proof.credId)) + + setProofItems(activeCreds) + } + }) + .catch((err: unknown) => { + const error = new BifoldError( + t('Error.Title1026'), + t('Error.Message1026'), + (err as Error)?.message ?? err, + 1026, + ) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + }) + }, []) + + const listHeader = () => { + return ( + + {loading ? ( + + + + ) : ( + {t('ProofRequest.MultipleCredentials')} + )} + + ) + } + + const changeCred = (credId: string) => { + onCredChange(credId) + navigation.goBack() + } + + return ( + + { + return ( + + + changeCred(item.credId ?? '')} + /> + + + ) + }}> + + ) +} + +export default ProofChangeCredentialW3C diff --git a/app/screens/ProofRequestW3C.tsx b/app/screens/ProofRequestW3C.tsx new file mode 100644 index 000000000..c176e9e11 --- /dev/null +++ b/app/screens/ProofRequestW3C.tsx @@ -0,0 +1,498 @@ +import type { StackScreenProps } from '@react-navigation/stack' + +import { + useConnectionById, + useProofById, + deleteConnectionRecordById, + acceptProofRequest, + declineProofRequest, + sendProofProblemReport, + GetCredentialsForRequestReturn, + DifPresentationExchangeProofFormatService, +} from '@adeya/ssi' +// eslint-disable-next-line import/no-extraneous-dependencies +import { DifPexCredentialsForRequestRequirement, SubmissionEntryCredential } from '@credo-ts/core' +import React, { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { DeviceEventEmitter, FlatList, ScrollView, StyleSheet, Text, View } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' +import Icon from 'react-native-vector-icons/MaterialIcons' + +import Button, { ButtonType } from '../components/buttons/Button' +import { CredentialCard } from '../components/misc' +import ConnectionImage from '../components/misc/ConnectionImage' +import CommonRemoveModal from '../components/modals/CommonRemoveModal' +import { EventTypes } from '../constants' +import { useAnimatedComponents } from '../contexts/animated-components' +import { useConfiguration } from '../contexts/configuration' +import { useNetwork } from '../contexts/network' +import { useTheme } from '../contexts/theme' +import { useOutOfBandByConnectionId } from '../hooks/connections' +import { useAllCredentialsForProof } from '../hooks/proofs' +import { BifoldError } from '../types/error' +import { NotificationStackParams, Screens, Stacks, TabStacks } from '../types/navigators' +import { ProofCredentialItems } from '../types/proof-items' +import { ModalUsage } from '../types/remove' +import { useAppAgent } from '../utils/agent' +import { testIdWithKey } from '../utils/testable' + +import ProofRequestAccept from './ProofRequestAccept' + +type ProofRequestProps = StackScreenProps + +interface CredentialListProps { + header?: JSX.Element + footer?: JSX.Element + items: ProofCredentialItems[] +} + +const ProofRequestW3C: React.FC = ({ navigation, route }) => { + if (!route?.params) { + throw new Error('ProofRequest route prams were not set properly') + } + + // eslint-disable-next-line no-unsafe-optional-chaining + const { proofId } = route?.params + const { agent } = useAppAgent() + const { t } = useTranslation() + const { assertConnectedNetwork } = useNetwork() + const proof = useProofById(proofId) + const connection = proof?.connectionId ? useConnectionById(proof.connectionId) : undefined + const proofConnectionLabel = connection?.theirLabel ?? proof?.connectionId ?? '' + const [pendingModalVisible, setPendingModalVisible] = useState(false) + const [retrievedCredentials, setRetrievedCredentials] = useState<{ + attributes: Record + predicates: Record + }>() + const [loading, setLoading] = useState(true) + const [declineModalVisible, setDeclineModalVisible] = useState(false) + const { ColorPallet, ListItems, TextTheme } = useTheme() + const { RecordLoading } = useAnimatedComponents() + const goalCode = useOutOfBandByConnectionId(agent, proof?.connectionId ?? '')?.outOfBandInvitation.goalCode + const { OCABundleResolver } = useConfiguration() + const [containsPI, setContainsPI] = useState(false) + const [activeCreds, setActiveCreds] = useState([]) + const [selectedCredentials, setSelectedCredentials] = useState([]) + const credProofPromise = useAllCredentialsForProof(proofId) + + const hasMatchingCredDef = useMemo( + () => activeCreds.some(cred => cred.credExchangeRecord !== undefined), + [activeCreds], + ) + const styles = StyleSheet.create({ + pageContainer: { + flex: 1, + }, + pageContent: { + flexGrow: 1, + justifyContent: 'space-between', + }, + pageMargin: { + marginHorizontal: 20, + }, + pageFooter: { + marginBottom: 15, + }, + headerTextContainer: { + paddingVertical: 16, + }, + headerText: { + ...ListItems.recordAttributeText, + flexShrink: 1, + }, + footerButton: { + paddingTop: 10, + }, + link: { + ...ListItems.recordAttributeText, + ...ListItems.recordLink, + paddingVertical: 2, + }, + valueContainer: { + minHeight: ListItems.recordAttributeText.fontSize, + paddingVertical: 4, + }, + detailsButton: { + ...ListItems.recordAttributeText, + color: ColorPallet.brand.link, + textDecorationLine: 'underline', + }, + cardLoading: { + backgroundColor: ColorPallet.brand.secondaryBackground, + flex: 1, + flexGrow: 1, + marginVertical: 35, + borderRadius: 15, + paddingHorizontal: 10, + }, + }) + + useEffect(() => { + if (!agent && !proof) { + DeviceEventEmitter.emit( + EventTypes.ERROR_ADDED, + new BifoldError(t('Error.Title1034'), t('Error.Message1034'), t('ProofRequest.ProofRequestNotFound'), 1034), + ) + } + }, []) + + useEffect(() => { + setLoading(true) + credProofPromise + ?.then(value => { + if (value) { + const { groupedProof, retrievedCredentials } = value + const retrievedCreds = retrievedCredentials as GetCredentialsForRequestReturn< + [DifPresentationExchangeProofFormatService] + >['proofFormats']['presentationExchange'] + setLoading(false) + let credList: string[] = [] + if (selectedCredentials.length > 0) { + credList = selectedCredentials + } else { + // we only want one of each satisfying credential + groupedProof.forEach(item => { + const credId = item.altCredentials?.[0] + if (credId && !credList.includes(credId)) { + credList.push(credId) + } + }) + } + + const formatCredentials = (retrievedItems: DifPexCredentialsForRequestRequirement[], credList: string[]) => { + return retrievedItems + .map(item => { + return { + [item.submissionEntry[0].inputDescriptorId]: item.submissionEntry[0].verifiableCredentials.filter( + cred => credList.includes(cred.credentialRecord.id), + ), + } + }) + .reduce((prev, curr) => { + return { + ...prev, + ...curr, + } + }, {}) + } + + const selectRetrievedCredentials = retrievedCreds + ? { + attributes: formatCredentials(retrievedCreds.requirements, credList), + predicates: {}, + } + : undefined + + setRetrievedCredentials(selectRetrievedCredentials) + + const activeCreds = groupedProof.filter(item => credList.includes(item.credId)) + setActiveCreds(activeCreds) + } + }) + .catch((err: unknown) => { + const error = new BifoldError( + t('Error.Title1026'), + t('Error.Message1026'), + (err as Error)?.message ?? err, + 1026, + ) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + }) + }, [selectedCredentials]) + + const toggleDeclineModalVisible = () => { + setDeclineModalVisible(!declineModalVisible) + } + + const getCredentialsFields = () => ({ + ...retrievedCredentials?.attributes, + ...retrievedCredentials?.predicates, + }) + + useEffect(() => { + // get oca bundle to see if we're presenting personally identifiable elements + activeCreds.some(async item => { + if (!item || !(item.credDefId || item.schemaId)) { + return false + } + const labels = (item.attributes ?? []).map(field => field.label ?? field.name ?? '') + const bundle = await OCABundleResolver.resolveAllBundles({ + identifiers: { credentialDefinitionId: item.credDefId, schemaId: item.schemaId }, + }) + const flaggedAttributes: string[] = (bundle as any).bundle.bundle.flaggedAttributes.map((attr: any) => attr.name) + const foundPI = labels.some(label => flaggedAttributes.includes(label)) + setContainsPI(foundPI) + return foundPI + }) + }, [activeCreds]) + + const hasAvailableCredentials = useMemo(() => { + const fields = getCredentialsFields() + + return !!retrievedCredentials && Object.values(fields).every(c => c.length > 0) + }, [retrievedCredentials]) + + const handleAcceptPress = async () => { + try { + if (!(agent && proof && assertConnectedNetwork())) { + return + } + setPendingModalVisible(true) + + if (!retrievedCredentials) { + throw new Error(t('ProofRequest.RequestedCredentialsCouldNotBeFound')) + } + + const proofCreds = { ...retrievedCredentials?.attributes } + + Object.keys(proofCreds).forEach(key => { + proofCreds[key] = [proofCreds[key][0].credentialRecord] + }) + + const proofFormats = { + presentationExchange: { + credentials: proofCreds, + }, + } + + if (!proofFormats) { + throw new Error(t('ProofRequest.RequestedCredentialsCouldNotBeFound')) + } + + await acceptProofRequest(agent, { + proofRecordId: proof.id, + proofFormats, + }) + if (proof.connectionId && goalCode && goalCode.endsWith('verify.once')) { + await deleteConnectionRecordById(agent, proof.connectionId) + } + } catch (err: unknown) { + setPendingModalVisible(false) + + const error = new BifoldError(t('Error.Title1027'), t('Error.Message1027'), (err as Error).message, 1027) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + } + } + + const handleDeclineTouched = async () => { + try { + if (proof) { + await declineProofRequest(agent, { proofRecordId: proof.id }) + + // sending a problem report fails if there is neither a connectionId nor a ~service decorator + if (proof.connectionId) { + await sendProofProblemReport(agent, { proofRecordId: proof.id, description: t('ProofRequest.Declined') }) + if (goalCode && goalCode.endsWith('verify.once')) { + await deleteConnectionRecordById(agent, proof.connectionId) + } + } + } + } catch (err: unknown) { + const error = new BifoldError(t('Error.Title1028'), t('Error.Message1028'), (err as Error).message, 1028) + + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + } + + toggleDeclineModalVisible() + + navigation.getParent()?.navigate(TabStacks.HomeStack, { screen: Screens.Home }) + } + + const proofPageHeader = () => { + return ( + + {loading ? ( + + + + ) : ( + <> + + + + {proofConnectionLabel || t('ContactDetails.AContact')}{' '} + {t('ProofRequest.IsRequestingYouToShare')} + {` ${activeCreds?.length} `} + {activeCreds?.length > 1 ? t('ProofRequest.Credentials') : t('ProofRequest.Credential')} + + {containsPI && ( + + + + {t('ProofRequest.SensitiveInformation')} + + + )} + + {!hasAvailableCredentials && hasMatchingCredDef && ( + + {t('ProofRequest.FromYourWallet')} + + )} + + )} + + ) + } + + const handleAltCredChange = (selectedCred: string, altCredentials: string[]) => { + const onCredChange = (cred: string) => { + const newSelectedCreds = ( + selectedCredentials.length > 0 ? selectedCredentials : activeCreds.map(item => item.credId) + ).filter(id => !altCredentials.includes(id)) + setSelectedCredentials([cred, ...newSelectedCreds]) + } + navigation.getParent()?.navigate(Stacks.ProofRequestsStack, { + screen: Screens.ProofChangeCredentialW3C, + params: { + selectedCred, + altCredentials, + proofId, + onCredChange, + }, + }) + } + + const proofPageFooter = () => { + return ( + + {!loading && proofConnectionLabel && goalCode !== 'aries.vc.verify.once' ? null : null} + +