diff --git a/packages/legacy/core/App/components/views/HomeFooterView.tsx b/packages/legacy/core/App/components/views/HomeFooterView.tsx index 97f080a887..a3b0c5b552 100644 --- a/packages/legacy/core/App/components/views/HomeFooterView.tsx +++ b/packages/legacy/core/App/components/views/HomeFooterView.tsx @@ -19,7 +19,7 @@ const HomeFooterView: React.FC = ({ children }) => { ...useCredentialByState(CredentialState.Done), ] const [{useNotifications}] = useServices([TOKENS.NOTIFICATIONS]) - const notifications = useNotifications() + const notifications = useNotifications({}) const { HomeTheme, TextTheme } = useTheme() const { t } = useTranslation() const styles = StyleSheet.create({ diff --git a/packages/legacy/core/App/container-api.ts b/packages/legacy/core/App/container-api.ts index 2f3964bb09..08e71f4e9d 100644 --- a/packages/legacy/core/App/container-api.ts +++ b/packages/legacy/core/App/container-api.ts @@ -1,10 +1,4 @@ -import { - Agent, - BaseLogger, - BasicMessageRecord, - ProofExchangeRecord, - CredentialExchangeRecord as CredentialRecord, -} from '@credo-ts/core' +import { Agent, BaseLogger } from '@credo-ts/core' import { IndyVdrPoolConfig } from '@credo-ts/indy-vdr' import { ProofRequestTemplate } from '@hyperledger/aries-bifold-verifier' import { OCABundleResolverType } from '@hyperledger/aries-oca/build/legacy' @@ -19,8 +13,9 @@ import Onboarding from './screens/Onboarding' import { AttestationMonitor } from './types/attestation' import { GenericFn } from './types/fn' import { AuthenticateStackParams, ScreenOptionsType } from './types/navigators' -import { CustomNotification, CustomNotificationRecord } from './types/notification' +import { CustomNotification } from './types/notification' import { Config } from './types/config' +import { NotificationReturnType, NotificationsInputProps } from './hooks/notifications' import { NotificationListItemProps } from './components/listItems/NotificationListItem' import { PINCreateHeaderProps } from './components/misc/PINCreateHeader' @@ -151,9 +146,7 @@ export type TokenMapping = { [TOKENS.LOAD_STATE]: LoadStateFn [TOKENS.COMP_BUTTON]: Button [TOKENS.NOTIFICATIONS]: { - useNotifications: () => Array< - BasicMessageRecord | CredentialRecord | ProofExchangeRecord | CustomNotificationRecord - > + useNotifications: ({ openIDUri }: NotificationsInputProps) => NotificationReturnType customNotificationConfig?: CustomNotification } [TOKENS.NOTIFICATIONS_LIST_ITEM]: React.FC diff --git a/packages/legacy/core/App/hooks/notifications.ts b/packages/legacy/core/App/hooks/notifications.ts index fa19da4a7c..ca677caaee 100644 --- a/packages/legacy/core/App/hooks/notifications.ts +++ b/packages/legacy/core/App/hooks/notifications.ts @@ -4,6 +4,8 @@ import { CredentialState, ProofExchangeRecord, ProofState, + SdJwtVcRecord, + W3cCredentialRecord, } from '@credo-ts/core' import { useBasicMessages, useCredentialByState, useProofByState } from '@credo-ts/react-hooks' import { ProofCustomMetadata, ProofMetadata } from '@hyperledger/aries-bifold-verifier' @@ -15,18 +17,27 @@ import { basicMessageCustomMetadata, credentialCustomMetadata, } from '../types/metadata' +import { useOpenID } from '../modules/openid/hooks/openid' +import { CustomNotification } from '../types/notification' -export const useNotifications = (): Array => { +export type NotificationsInputProps = { + openIDUri?: string +} + +export type NotificationReturnType = Array< + BasicMessageRecord | CredentialRecord | ProofExchangeRecord | CustomNotification | SdJwtVcRecord | W3cCredentialRecord +> + +export const useNotifications = ({ openIDUri }: NotificationsInputProps): NotificationReturnType => { const { records: basicMessages } = useBasicMessages() - const [notifications, setNotifications] = useState<(BasicMessageRecord | CredentialRecord | ProofExchangeRecord)[]>( - [] - ) + const [notifications, setNotifications] = useState([]) const credsReceived = useCredentialByState(CredentialState.CredentialReceived) const credsDone = useCredentialByState(CredentialState.Done) const proofsDone = useProofByState([ProofState.Done, ProofState.PresentationReceived]) const offers = useCredentialByState(CredentialState.OfferReceived) const proofsRequested = useProofByState(ProofState.RequestReceived) + const openIDCredRecieved = useOpenID({ openIDUri: openIDUri }) useEffect(() => { // get all unseen messages @@ -58,11 +69,21 @@ export const useNotifications = (): Array new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ) + const openIDCreds: Array = [] + if (openIDCredRecieved) { + openIDCreds.push(openIDCredRecieved) + } + + const notif = [ + ...messagesToShow, + ...offers, + ...proofsRequested, + ...validProofsDone, + ...revoked, + ...openIDCreds, + ].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) setNotifications(notif) - }, [basicMessages, credsReceived, proofsDone, proofsRequested, offers, credsDone]) + }, [basicMessages, credsReceived, proofsDone, proofsRequested, offers, credsDone, openIDCredRecieved]) return notifications } diff --git a/packages/legacy/core/App/modules/openid/components/OpenIDCredentialCard.tsx b/packages/legacy/core/App/modules/openid/components/OpenIDCredentialCard.tsx new file mode 100644 index 0000000000..5f675c2043 --- /dev/null +++ b/packages/legacy/core/App/modules/openid/components/OpenIDCredentialCard.tsx @@ -0,0 +1,234 @@ +import React from 'react' +import { TouchableOpacity } from 'react-native-gesture-handler' +import { DisplayImage, W3cCredentialDisplay } from '../types' +import { useTranslation } from 'react-i18next' +import { GenericFn } from '../../../types/fn' +import { Image, ImageBackground, StyleSheet, Text, useWindowDimensions, View, ViewStyle } from 'react-native' +import { testIdWithKey } from '../../../utils/testable' +import { credentialTextColor, toImageSource } from '../../../utils/credential' +import { useTheme } from '../../../contexts/theme' +import { formatTime } from '../../../utils/helpers' +import { SvgUri } from 'react-native-svg' + +interface CredentialCard10Props { + credentialDisplay: W3cCredentialDisplay + onPress?: GenericFn + style?: ViewStyle +} + +const paddingVertical = 10 +const paddingHorizontal = 10 +const transparent = 'rgba(0,0,0,0)' +const borderRadius = 15 +const borderPadding = 8 + +const OpenIDCredentialCard: React.FC = ({ credentialDisplay, style = {}, onPress = undefined }) => { + + const { t } = useTranslation() + const { ColorPallet, TextTheme } = useTheme() + const {display} = credentialDisplay + + const { width } = useWindowDimensions() + const cardHeight = width / 2 // a card height is half of the screen width + const cardHeaderHeight = cardHeight / 4 // a card has a total of 4 rows, and the header occupy 1 row + + const styles = StyleSheet.create({ + container: { + backgroundColor: display.backgroundColor ? display.backgroundColor : transparent, + height: cardHeight, + borderRadius: borderRadius, + }, + outerHeaderContainer: { + flexDirection: 'column', + backgroundColor: transparent, + height: cardHeaderHeight + borderPadding, + borderTopLeftRadius: borderRadius, + borderTopRightRadius: borderRadius, + }, + innerHeaderContainer: { + flexDirection: 'row', + height: cardHeaderHeight, + marginLeft: borderPadding, + marginRight: borderPadding, + marginTop: borderPadding, + marginBottom: borderPadding, + backgroundColor: transparent, + }, + bodyContainer: { + flexGrow: 1, + }, + footerContainer: { + flexDirection: 'row', + backgroundColor: transparent, + paddingHorizontal, + paddingVertical, + borderBottomLeftRadius: borderRadius, + borderBottomRightRadius: borderRadius, + }, + revokedFooter: { + backgroundColor: ColorPallet.notification.error, + flexGrow: 1, + marginHorizontal: -1 * paddingHorizontal, + marginVertical: -1 * paddingVertical, + paddingHorizontal: paddingHorizontal, + paddingVertical: paddingVertical, + borderBottomLeftRadius: borderRadius, + borderBottomRightRadius: borderRadius, + }, + flexGrow: { + flexGrow: 1, + }, + watermark: { + opacity: 0.16, + fontSize: 22, + transform: [{ rotate: '-30deg' }], + }, + }) + + const logoContaineter = (logo: DisplayImage | undefined) => { + const width = 64 + const height = 48 + const src = logo?.url + if(!src) { + return + } + if (typeof src === 'string' && src.endsWith('.svg')) + return + + return ( + + ) + } + + + const CardHeader: React.FC = () => { + return ( + + + {logoContaineter(display.issuer.logo)} + + + + {display.name} + + + + + {display.description} + + + + + + ) + } + + const CardBody: React.FC = () => { + return + } + + const CardFooter: React.FC = () => { + return ( + + + {t('CredentialDetails.Issued')}: {formatTime(new Date(), { shortMonth: true })} + + + ) + } + const CredentialCard: React.FC = () => { + return ( + <> + + + + + ) + } + + return ( + + + {display.backgroundImage ? ( + + + + ) : ( + + )} + + + ) +} + +export default OpenIDCredentialCard \ No newline at end of file diff --git a/packages/legacy/core/App/modules/openid/display.tsx b/packages/legacy/core/App/modules/openid/display.tsx new file mode 100644 index 0000000000..22aef38f57 --- /dev/null +++ b/packages/legacy/core/App/modules/openid/display.tsx @@ -0,0 +1,251 @@ +import type { CredentialDisplay, CredentialIssuerDisplay, JffW3cCredentialJson, OpenId4VcCredentialMetadata, W3cCredentialDisplay, W3cCredentialJson } from './types' +import type { W3cCredentialRecord } from '@credo-ts/core' + +import { Hasher, SdJwtVcRecord, ClaimFormat, JsonTransformer } from '@credo-ts/core' +import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode' +import { CredentialForDisplayId } from './types' +import { getHostNameFromUrl, sanitizeString } from './utils/utils' +import { getOpenId4VcCredentialMetadata } from './metadata' + + +function findDisplay(display?: Display[]): Display | undefined { + if (!display) return undefined + + let item = display.find((d) => d.locale?.startsWith('en-')) + if (!item) item = display.find((d) => !d.locale) + if (!item) item = display[0] + + return item +} + +function getIssuerDisplay(metadata: OpenId4VcCredentialMetadata | null | undefined): Partial { + const issuerDisplay: Partial = {} + // Try to extract from openid metadata first + const openidIssuerDisplay = findDisplay(metadata?.issuer.display) + issuerDisplay.name = openidIssuerDisplay?.name + issuerDisplay.logo = openidIssuerDisplay?.logo ? { + url: openidIssuerDisplay.logo?.url, + altText: openidIssuerDisplay.logo?.alt_text, + } : undefined + + // If the credentialDisplay contains a logo, and the issuerDisplay does not, use the logo from the credentialDisplay + const openidCredentialDisplay = findDisplay(metadata?.credential.display) + if (openidCredentialDisplay && !issuerDisplay.logo && openidCredentialDisplay.logo) { + issuerDisplay.logo = { + url: openidCredentialDisplay.logo?.url, + altText: openidCredentialDisplay.logo?.alt_text, + } + } + + return issuerDisplay +} + +function processIssuerDisplay(metadata:OpenId4VcCredentialMetadata | null | undefined, issuerDisplay: Partial): CredentialIssuerDisplay { + // Last fallback: use issuer id from openid4vc + if (!issuerDisplay.name && metadata?.issuer.id) { + issuerDisplay.name = getHostNameFromUrl(metadata.issuer.id) + } + + return { + ...issuerDisplay, + name: issuerDisplay.name ?? 'Unknown', + } +} + +function getW3cIssuerDisplay( + credential: W3cCredentialJson, + openId4VcMetadata?: OpenId4VcCredentialMetadata | null +): CredentialIssuerDisplay { + const issuerDisplay: Partial = getIssuerDisplay(openId4VcMetadata) + + // If openid metadata is not available, try to extract display metadata from the credential based on JFF metadata + const jffCredential = credential as JffW3cCredentialJson + const issuerJson = typeof jffCredential.issuer === 'string' ? undefined : jffCredential.issuer + + // Issuer Display from JFF + if (!issuerDisplay.logo || !issuerDisplay.logo.url) { + issuerDisplay.logo = issuerJson?.logoUrl ? { url: issuerJson?.logoUrl } + : ( issuerJson?.image ? { url: typeof issuerJson.image === 'string' ? issuerJson.image : issuerJson.image.id } : undefined ) + } + + // Issuer name from JFF + if (!issuerDisplay.name) { + issuerDisplay.name = issuerJson?.name + } + + return processIssuerDisplay(openId4VcMetadata, issuerDisplay) +} + +function getSdJwtIssuerDisplay(openId4VcMetadata?: OpenId4VcCredentialMetadata | null): CredentialIssuerDisplay { + const issuerDisplay: Partial = getIssuerDisplay(openId4VcMetadata) + + return processIssuerDisplay(openId4VcMetadata, issuerDisplay) +} + +function getCredentialDisplay( + credentialPayload: Record, + openId4VcMetadata?: OpenId4VcCredentialMetadata | null +): Partial { + const credentialDisplay: Partial = {} + + if (openId4VcMetadata) { + const openidCredentialDisplay = findDisplay(openId4VcMetadata.credential.display) + credentialDisplay.name = openidCredentialDisplay?.name + credentialDisplay.description = openidCredentialDisplay?.description + credentialDisplay.textColor = openidCredentialDisplay?.text_color + credentialDisplay.backgroundColor = openidCredentialDisplay?.background_color + credentialDisplay.backgroundImage = openidCredentialDisplay?.background_image ? { + url: openidCredentialDisplay.background_image.url, + altText: openidCredentialDisplay.background_image.alt_text, + } : undefined + } + + return credentialDisplay +} + +function getW3cCredentialDisplay( + credential: W3cCredentialJson, + openId4VcMetadata?: OpenId4VcCredentialMetadata | null +) { + const credentialDisplay: Partial = getCredentialDisplay(credential, openId4VcMetadata) + + // If openid metadata is not available, try to extract display metadata from the credential based on JFF metadata + const jffCredential = credential as JffW3cCredentialJson + + if (!credentialDisplay.name) { + credentialDisplay.name = jffCredential.name + } + + // If there's no name for the credential, we extract it from the last type + // and sanitize it. This is not optimal. But provides at least something. + if (!credentialDisplay.name && jffCredential.type.length > 1) { + const lastType = jffCredential.type[jffCredential.type.length - 1] + credentialDisplay.name = (lastType && !lastType.startsWith('http')) ? sanitizeString(lastType) : undefined + } + + // Use background color from the JFF credential if not provided by the OID4VCI metadata + if (!credentialDisplay.backgroundColor && jffCredential.credentialBranding?.backgroundColor) { + credentialDisplay.backgroundColor = jffCredential.credentialBranding.backgroundColor + } + + return { + ...credentialDisplay, + // Last fallback, if there's really no name for the credential, we use a generic name + name: credentialDisplay.name ?? 'Credential', + } +} + +function getSdJwtCredentialDisplay( + credentialPayload: Record, + openId4VcMetadata?: OpenId4VcCredentialMetadata | null +) { + const credentialDisplay: Partial = getCredentialDisplay(credentialPayload, openId4VcMetadata) + + if (!credentialDisplay.name && typeof credentialPayload.vct === 'string') { + credentialDisplay.name = sanitizeString(credentialPayload.vct) + } + + return { + ...credentialDisplay, + name: credentialDisplay.name ?? 'Credential', + } +} + +interface CredentialMetadata { + type: string + issuer: string + holder: string | Record + validUntil?: Date + validFrom?: Date + issuedAt?: Date +} + +export function filterAndMapSdJwtKeys(sdJwtVcPayload: Record) { + type SdJwtVcPayload = { + iss: string + cnf: Record + vct: string + iat?: number + nbf?: number + exp?: number + [key: string]: unknown + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { _sd_alg, _sd_hash, iss, vct, cnf, iat, exp, nbf, ...visibleProperties } = sdJwtVcPayload as SdJwtVcPayload + + const credentialMetadata: CredentialMetadata = { + type: vct, + issuer: iss, + holder: cnf, + } + + if (iat) { + credentialMetadata.issuedAt = new Date(iat * 1000) + } + if (exp) { + credentialMetadata.validUntil = new Date(exp * 1000) + } + if (nbf) { + credentialMetadata.validFrom = new Date(nbf * 1000) + } + + return { + visibleProperties, + metadata: credentialMetadata, + } +} + +function getCredentialForDisplaySdJwt(credentialRecord: SdJwtVcRecord, metadata: OpenId4VcCredentialMetadata | null): W3cCredentialDisplay { + + const { disclosures, jwt } = decodeSdJwtSync(credentialRecord.compactSdJwtVc, (data, alg) => Hasher.hash(data, alg)) + const decodedPayload: Record = getClaimsSync(jwt.payload, disclosures, (data, alg) => + Hasher.hash(data, alg) + ) + + const issuerDisplay = getSdJwtIssuerDisplay(metadata) + const credentialDisplay = getSdJwtCredentialDisplay(decodedPayload, metadata) + + return { + id: `sd-jwt-vc-${credentialRecord.id}` satisfies CredentialForDisplayId, + createdAt: credentialRecord.createdAt, + display: { + ...credentialDisplay, + issuer: issuerDisplay, + }, + attributes: filterAndMapSdJwtKeys(decodedPayload).visibleProperties, + } +} + +export function getCredentialForDisplay(credentialRecord: W3cCredentialRecord | SdJwtVcRecord): W3cCredentialDisplay { + + const openId4VcMetadata = getOpenId4VcCredentialMetadata(credentialRecord) + + if (credentialRecord instanceof SdJwtVcRecord) { + return getCredentialForDisplaySdJwt(credentialRecord, openId4VcMetadata) + } + + const credential = JsonTransformer.toJSON( + credentialRecord.credential.claimFormat === ClaimFormat.JwtVc + ? credentialRecord.credential.credential + : credentialRecord.credential + ) as W3cCredentialJson + + const issuerDisplay = getW3cIssuerDisplay(credential, openId4VcMetadata) + const credentialDisplay = getW3cCredentialDisplay(credential, openId4VcMetadata) + + // to be implimented later support credential with multiple subjects + const credentialAttributes = Array.isArray(credential.credentialSubject) + ? credential.credentialSubject[0] ?? {} + : credential.credentialSubject + + return { + id: `w3c-credential-${credentialRecord.id}` satisfies CredentialForDisplayId, + createdAt: credentialRecord.createdAt, + display: { + ...credentialDisplay, + issuer: issuerDisplay, + }, + credential, + attributes: credentialAttributes, + } +} diff --git a/packages/legacy/core/App/modules/openid/hooks/openid.tsx b/packages/legacy/core/App/modules/openid/hooks/openid.tsx new file mode 100644 index 0000000000..0846b29c2a --- /dev/null +++ b/packages/legacy/core/App/modules/openid/hooks/openid.tsx @@ -0,0 +1,49 @@ +import { SdJwtVcRecord, W3cCredentialRecord } from "@credo-ts/core" +import { useEffect, useState } from "react" +import { receiveCredentialFromOpenId4VciOffer } from "../resolver" +import { DeviceEventEmitter } from "react-native" +import { EventTypes } from "../../../constants" +import { BifoldError } from "../../../types/error" +import { useAgent } from "@credo-ts/react-hooks" +import { useTranslation } from "react-i18next" + +type OpenIDContextProps = { + openIDUri?: string + } + +export const useOpenID = ({openIDUri}: OpenIDContextProps): SdJwtVcRecord | W3cCredentialRecord | undefined => { + const [openIdRecord, setOpenIdRecord] = useState() + + const { agent } = useAgent() + const { t } = useTranslation() + + const resolveOpenIDCredential = async (uri: string) => { + if(!agent) { + return + } + try{ + const record = await receiveCredentialFromOpenId4VciOffer({ + agent: agent, + uri: uri + }) + return record + } catch (err: unknown) { + //TODO: Sppecify different error + const error = new BifoldError(t('Error.Title1043'), t('Error.Message1043'), (err as Error)?.message ?? err, 1043) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + } + } + + useEffect(() => { + if (!openIDUri) { + return + } + resolveOpenIDCredential(openIDUri).then((value) => { + if(value){ + setOpenIdRecord(value) + } + }) + }, [openIDUri]) + + return openIdRecord +} \ No newline at end of file diff --git a/packages/legacy/core/App/modules/openid/resolver.tsx b/packages/legacy/core/App/modules/openid/resolver.tsx index 727621babb..990f437386 100644 --- a/packages/legacy/core/App/modules/openid/resolver.tsx +++ b/packages/legacy/core/App/modules/openid/resolver.tsx @@ -7,7 +7,9 @@ import { KeyDidCreateOptions, KeyType, SdJwtVcRecord, + SdJwtVcRepository, W3cCredentialRecord, + W3cCredentialRepository, getJwkFromKey, } from '@credo-ts/core' import { OpenId4VciCredentialFormatProfile, OpenId4VciCredentialSupportedWithId, OpenId4VciSupportedCredentialFormats } from '@credo-ts/openid4vc' @@ -151,6 +153,14 @@ export const receiveCredentialFromOpenId4VciOffer = async ({ agent, data, uri }: ) setOpenId4VcCredentialMetadata(record, openId4VcMetadata) - console.log("$$openID Cred:", JSON.stringify(record)) return record } + + +export async function storeCredential(agent: Agent, credentialRecord: W3cCredentialRecord | SdJwtVcRecord) { + if (credentialRecord instanceof W3cCredentialRecord) { + await agent.dependencyManager.resolve(W3cCredentialRepository).save(agent.context, credentialRecord) + } else { + await agent.dependencyManager.resolve(SdJwtVcRepository).save(agent.context, credentialRecord) + } +} diff --git a/packages/legacy/core/App/modules/openid/screens/OpenIDCredentialOffer.tsx b/packages/legacy/core/App/modules/openid/screens/OpenIDCredentialOffer.tsx new file mode 100644 index 0000000000..97b3411f1a --- /dev/null +++ b/packages/legacy/core/App/modules/openid/screens/OpenIDCredentialOffer.tsx @@ -0,0 +1,171 @@ +import React from 'react' +import { StackScreenProps } from "@react-navigation/stack" +import { DeliveryStackParams, Screens, TabStacks } from "../../../types/navigators" +import { getCredentialForDisplay } from "../display" +import { SafeAreaView } from "react-native-safe-area-context" +import CommonRemoveModal from "../../../components/modals/CommonRemoveModal" +import { ModalUsage } from "../../../types/remove" +import { useState } from "react" +import { DeviceEventEmitter, FlatList, StyleSheet, Text, View } from "react-native" +import { TextTheme } from "../../../theme" +import { useTranslation } from "react-i18next" +import Button, { ButtonType } from "../../../components/buttons/Button" +import { testIdWithKey } from "../../../utils/testable" +import RecordHeader from "../../../components/record/RecordHeader" +import RecordFooter from "../../../components/record/RecordFooter" +import { useTheme } from "../../../contexts/theme" +import OpenIDCredentialCard from '../components/OpenIDCredentialCard' +import { buildFieldsFromOpenIDTemplate } from '../utils/utils' +import RecordField from '../../../components/record/RecordField' +import { BifoldError } from '../../../types/error' +import { EventTypes } from '../../../constants' +import { useAgent } from '@credo-ts/react-hooks' +import { storeCredential } from '../resolver' +import CredentialOfferAccept from '../../../screens/CredentialOfferAccept' + +type OpenIDCredentialDetailsProps = StackScreenProps + +const OpenIDCredentialDetails: React.FC = ({ navigation, route }) => { + + + const { credential } = route.params + const credentialDisplay = getCredentialForDisplay(credential) + const { display, attributes } = credentialDisplay + const fields = buildFieldsFromOpenIDTemplate(attributes) + const { t } = useTranslation() + const { ColorPallet } = useTheme() + const { agent } = useAgent() + + const [declineModalVisible, setDeclineModalVisible] = useState(false) + const [buttonsVisible, setButtonsVisible] = useState(true) + const [acceptModalVisible, setAcceptModalVisible] = useState(false) + + const styles = StyleSheet.create({ + headerTextContainer: { + paddingHorizontal: 25, + paddingVertical: 16, + }, + headerText: { + ...TextTheme.normal, + flexShrink: 1, + }, + footerButton: { + paddingTop: 10, + }, + }) + + const toggleDeclineModalVisible = () => setDeclineModalVisible(!declineModalVisible) + + const handleDeclineTouched = async () => { + toggleDeclineModalVisible() + navigation.getParent()?.navigate(TabStacks.HomeStack, { screen: Screens.Home }) + } + + const handleAcceptTouched = async () => { + try { + if(!agent) { + return + } + await storeCredential(agent, credential) + setAcceptModalVisible(true) + } catch (err: unknown) { + setButtonsVisible(true) + const error = new BifoldError(t('Error.Title1025'), t('Error.Message1025'), (err as Error)?.message ?? err, 1025) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + } + } + + const footerButton = ( + title: string, + buttonPress: () => void, + buttonType: ButtonType, + testID: string, + accessibilityLabel: string + ) => { + return ( + +