diff --git a/packages/legacy/core/App/components/misc/CredentialCard11.tsx b/packages/legacy/core/App/components/misc/CredentialCard11.tsx index 339be96fca..dd9cdcee56 100644 --- a/packages/legacy/core/App/components/misc/CredentialCard11.tsx +++ b/packages/legacy/core/App/components/misc/CredentialCard11.tsx @@ -12,7 +12,7 @@ import { useConfiguration } from '../../contexts/configuration' import { useTheme } from '../../contexts/theme' import { GenericFn } from '../../types/fn' import { credentialTextColor, getCredentialIdentifiers, toImageSource } from '../../utils/credential' -import { getCredentialConnectionLabel, isDataUrl } from '../../utils/helpers' +import { formatIfDate, getCredentialConnectionLabel, isDataUrl, pTypeToText } from '../../utils/helpers' import { testIdWithKey } from '../../utils/testable' import CardWatermark from './CardWatermark' @@ -98,6 +98,15 @@ const CredentialCard11: React.FC = ({ (field) => field.name === overlay?.brandingOverlay?.secondaryAttribute ) + const attributeTypes = overlay.bundle?.captureBase.attributes + const attributeFormats: Record = (overlay.bundle as any)?.bundle.attributes + .map((attr: any) => { + return { name: attr.name, format: attr.format } + }) + .reduce((prev: { [key: string]: string }, curr: { name: string; format?: string }) => { + return { ...prev, [curr.name]: curr.format } + }, {}) + const cardData = [...(displayItems ?? []), primaryField, secondaryField] const getSecondaryBackgroundColor = () => { @@ -137,6 +146,9 @@ const CredentialCard11: React.FC = ({ resizeMode: 'contain', borderRadius: 10, }, + attributeValueContainer: { + width: '90%', + }, statusContainer: { backgroundColor: 'rgba(0, 0, 0, 0)', borderTopRightRadius: borderRadius, @@ -194,7 +206,18 @@ const CredentialCard11: React.FC = ({ }) const parseAttribute = (item: (Attribute & Predicate) | undefined) => { - return { label: item?.label ?? item?.name ?? '', value: item?.value || `${item?.pType} ${item?.pValue}` } + let parsedItem = item + if (item && !item.value) { + parsedItem = pTypeToText(item, t, attributeTypes) as Attribute & Predicate + } + const parsedValue = formatIfDate( + attributeFormats?.[item?.name ?? ''], + parsedItem?.value ?? parsedItem?.pValue ?? null + ) + return { + label: item?.label ?? item?.name ?? '', + value: item?.value ? parsedValue : `${parsedItem?.pType} ${parsedValue}`, + } } useEffect(() => { @@ -303,20 +326,22 @@ const CredentialCard11: React.FC = ({ {isDataUrl(value) ? ( ) : ( - - {value} - + + + {value} + + )} ) @@ -324,6 +349,7 @@ const CredentialCard11: React.FC = ({ const renderCardAttribute = (item: Attribute & Predicate) => { const { label, value } = parseAttribute(item) + const parsedValue = formatIfDate(item?.format, value) ?? '' return ( item && ( @@ -350,7 +376,7 @@ const CredentialCard11: React.FC = ({ size={ListItems.recordAttributeText.fontSize} /> )} - + )} {item?.satisfied != undefined && item?.satisfied === false ? ( diff --git a/packages/legacy/core/App/components/misc/SharedProofData.tsx b/packages/legacy/core/App/components/misc/SharedProofData.tsx index d2c3a714a1..90157f4f75 100644 --- a/packages/legacy/core/App/components/misc/SharedProofData.tsx +++ b/packages/legacy/core/App/components/misc/SharedProofData.tsx @@ -14,6 +14,7 @@ import { import { useConfiguration } from '../../contexts/configuration' import { useTheme } from '../../contexts/theme' import { toImageSource } from '../../utils/credential' +import { formatIfDate, pTypeToText } from '../../utils/helpers' import { buildFieldsFromSharedAnonCredsProof } from '../../utils/oca' import { testIdWithKey } from '../../utils/testable' import LoadingIndicator from '../animated/LoadingIndicator' @@ -59,6 +60,7 @@ const SharedDataCard: React.FC<{ sharedData: GroupedSharedProofDataItem }> = ({ elevation: 5, }, cardAttributes: { + width: '65%', paddingTop: 20, paddingBottom: 10, }, @@ -79,6 +81,15 @@ const SharedDataCard: React.FC<{ sharedData: GroupedSharedProofDataItem }> = ({ const [overlay, setOverlay] = useState | undefined>(undefined) + const attributeTypes = overlay?.bundle?.captureBase.attributes + const attributeFormats: Record = (overlay?.bundle as any)?.bundle.attributes + .map((attr: any) => { + return { name: attr.name, format: attr.format } + }) + .reduce((prev: { [key: string]: string }, curr: { name: string; format?: string }) => { + return { ...prev, [curr.name]: curr.format } + }, {}) + useEffect(() => { const attributes = buildFieldsFromSharedAnonCredsProof(sharedData.data) const params = { @@ -95,14 +106,21 @@ const SharedDataCard: React.FC<{ sharedData: GroupedSharedProofDataItem }> = ({ }, [sharedData]) const CardField: React.FC<{ item: Field }> = ({ item }) => { + const { t } = useTranslation() + let parsedPredicate: Predicate | undefined = undefined + if (item instanceof Predicate) { + parsedPredicate = pTypeToText(item, t, attributeTypes) as Predicate + parsedPredicate.pValue = formatIfDate(attributeFormats[item.name ?? ''], parsedPredicate.pValue) + } else { + ;(item as Attribute).value = formatIfDate(attributeFormats[item.name ?? ''], (item as Attribute).value) + } + return ( - + {item.label || item.name} {item instanceof Attribute && } {item instanceof Predicate && ( - - {item.pType} {item.pValue} - + {`${parsedPredicate?.pType} ${parsedPredicate?.pValue}`} )} ) diff --git a/packages/legacy/core/App/components/record/RecordField.tsx b/packages/legacy/core/App/components/record/RecordField.tsx index 408c01d69d..1d135b1054 100644 --- a/packages/legacy/core/App/components/record/RecordField.tsx +++ b/packages/legacy/core/App/components/record/RecordField.tsx @@ -45,7 +45,7 @@ export const AttributeValue: React.FC = ({ field, style, s ) { return } - if (field.type == CaptureBaseAttributeType.DateInt) { + if (field.type == CaptureBaseAttributeType.DateInt || field.type == CaptureBaseAttributeType.DateTime) { return } return ( diff --git a/packages/legacy/core/App/localization/en/index.ts b/packages/legacy/core/App/localization/en/index.ts index f4375359bd..73caa8eed1 100644 --- a/packages/legacy/core/App/localization/en/index.ts +++ b/packages/legacy/core/App/localization/en/index.ts @@ -411,6 +411,12 @@ const translation = { "DeleteOfferDescription": "Don't recognize the organization? Check your Contacts list. You only receive notifications from Contacts you've initiated", }, "ProofRequest": { + "PredicateGeDate": "is after", + "PredicateLeDate": "is before", + "PredicateGe": "is greater than or equal to", + "PredicateGr": "is greater than", + "PredicateLe": "is less than or equal to", + "PredicateLs": "is less than", "ProofRequest": "Proof Request", "RequestProcessing": "Just a moment...", "OfferDelay": "Offer delay", diff --git a/packages/legacy/core/App/localization/fr/index.ts b/packages/legacy/core/App/localization/fr/index.ts index 9856c149eb..0616aef511 100644 --- a/packages/legacy/core/App/localization/fr/index.ts +++ b/packages/legacy/core/App/localization/fr/index.ts @@ -409,6 +409,12 @@ const translation = { "CustomOfferParagraph2": "Vous ne reconnaissez pas l'organisation? Vérifiez votre liste de Contacts. Vous ne recevez des notifications que des Contacts que vous avez initiés." }, "ProofRequest": { + "PredicateGeDate": "is after (FR)", + "PredicateLeDate": "is before (FR)", + "PredicateGe": "is greater than or equal to (FR)", + "PredicateGr": "is greater than (FR)", + "PredicateLe": "is less than or equal to (FR)", + "PredicateLs": "is less than (FR)", "ProofRequest": "Demande de preuve", "RequestProcessing": "Juste un instant...", "OfferDelay": "Retard de l'offre", diff --git a/packages/legacy/core/App/localization/pt-br/index.ts b/packages/legacy/core/App/localization/pt-br/index.ts index 101ee0c24b..ec73b665e7 100644 --- a/packages/legacy/core/App/localization/pt-br/index.ts +++ b/packages/legacy/core/App/localization/pt-br/index.ts @@ -391,6 +391,12 @@ const translation = { "CustomOfferParagraph2": "Não reconhece a organização. Verifique sua lista de Contatos. Você só recebe notificões de Contatos que você tenha adicionado.", }, "ProofRequest": { + "PredicateGeDate": "is after (PB)", + "PredicateLeDate": "is before (PB)", + "PredicateGe": "is greater than or equal to (PB)", + "PredicateGr": "is greater than (PB)", + "PredicateLe": "is less than or equal to (PB)", + "PredicateLs": "is less than (PB)", "ProofRequest": "Requisição de Prova", "RequestProcessing": "Só um momento...", "OfferDelay": "Atrasar oferta", diff --git a/packages/legacy/core/App/screens/ProofRequestDetails.tsx b/packages/legacy/core/App/screens/ProofRequestDetails.tsx index da3d239719..0c79492a7f 100644 --- a/packages/legacy/core/App/screens/ProofRequestDetails.tsx +++ b/packages/legacy/core/App/screens/ProofRequestDetails.tsx @@ -21,7 +21,7 @@ import { useStore } from '../contexts/store' import { useTheme } from '../contexts/theme' import { useTemplate } from '../hooks/proof-request-templates' import { Screens, ProofRequestsStackParams } from '../types/navigators' -import { formatIfDate } from '../utils/helpers' +import { formatIfDate, pTypeToText } from '../utils/helpers' import { buildFieldsFromAnonCredsProofRequestTemplate } from '../utils/oca' import { parseSchemaFromId } from '../utils/schema' import { testIdWithKey } from '../utils/testable' @@ -37,7 +37,7 @@ const AttributeItem: React.FC<{ item: Attribute; style?: StyleProp }> const [value, setValue] = useState(item.value) useEffect(() => { - formatIfDate(item.format, value, setValue) + setValue(formatIfDate(item.format, value)) }, []) return ( @@ -54,14 +54,6 @@ const PredicateItem: React.FC<{ onChangeValue: (name: string, value: string) => void }> = ({ item, style, onChangeValue }) => { const { ColorPallet } = useTheme() - const [pValue, setPValue] = useState(item.pValue) - - useEffect(() => { - // can't format the date if parameterizable, must remain a number - if (!item.parameterizable) { - formatIfDate(item.format, pValue, setPValue) - } - }, []) const defaultStyle = StyleSheet.create({ input: { @@ -72,7 +64,7 @@ const PredicateItem: React.FC<{ }) return ( - + {item.label || item.name} {item.pType} {item.parameterizable && ( @@ -81,17 +73,17 @@ const PredicateItem: React.FC<{ style={[style, defaultStyle.input]} onChangeText={(value) => onChangeValue(item.name || '', value)} > - {pValue} + {item.pValue} )} - {!item.parameterizable && {pValue}} + {!item.parameterizable && {item.pValue}} ) } const ProofRequestAttributesCard: React.FC = ({ data, onChangeValue }) => { const { ListItems, ColorPallet } = useTheme() - const { i18n } = useTranslation() + const { t, i18n } = useTranslation() const { OCABundleResolver } = useConfiguration() const style = StyleSheet.create({ @@ -113,16 +105,18 @@ const ProofRequestAttributesCard: React.FC = ( ...ListItems.requestTemplateTitle, fontWeight: 'bold', fontSize: 18, - paddingVertical: 8, marginRight: 8, }, attributesList: { paddingLeft: 14, }, + fieldContainer: { flexDirection: 'row', paddingVertical: 8 }, }) const [meta, setMeta] = useState(undefined) const [attributes, setAttributes] = useState(undefined) + const [attributeTypes, setAttributeTypes] = useState | undefined>(undefined) + const [attributeFormats, setAttributeFormats] = useState | undefined>(undefined) useEffect(() => { OCABundleResolver.resolve({ identifiers: { schemaId: data.schema }, language: i18n.language }).then((bundle) => { @@ -142,9 +136,6 @@ const ProofRequestAttributesCard: React.FC = ( }) setMeta(metaOverlay) }) - }, [data.schema]) - - useEffect(() => { const attributes = buildFieldsFromAnonCredsProofRequestTemplate(data) OCABundleResolver.presentationFields({ identifiers: { schemaId: data.schema }, @@ -155,6 +146,32 @@ const ProofRequestAttributesCard: React.FC = ( }) }, [data.schema]) + useEffect(() => { + const credDefId = (data.requestedAttributes ?? data.requestedPredicates) + ?.flatMap((reqItem) => reqItem.restrictions?.map((restrictionItem) => restrictionItem.cred_def_id)) + .find((item) => item !== undefined) + const params = { + identifiers: { + credentialDefinitionId: credDefId, + schemaId: data.schema, + }, + language: i18n.language, + attributes, + } + OCABundleResolver.resolveAllBundles(params).then((bundle) => { + setAttributeTypes(bundle?.bundle?.captureBase.attributes) + setAttributeFormats( + (bundle.bundle as any)?.bundle.attributes + .map((attr: any) => { + return { name: attr.name, format: attr.format } + }) + .reduce((prev: { [key: string]: string }, curr: { name: string; format?: string }) => { + return { ...prev, [curr.name]: curr.format } + }, {}) + ) + }) + }, []) + return ( {meta?.name} @@ -163,13 +180,21 @@ const ProofRequestAttributesCard: React.FC = ( data={attributes} keyExtractor={(record, index) => record.name || index.toString()} renderItem={({ item }) => { + item.format = attributeFormats?.[item.name ?? ''] + let parsedPredicate: Predicate | undefined = undefined + if (item instanceof Predicate) { + parsedPredicate = pTypeToText(item, t, attributeTypes) as Predicate + if (!parsedPredicate.parameterizable) { + parsedPredicate.pValue = formatIfDate(parsedPredicate.format, parsedPredicate.pValue) + } + } return ( - + {`\u2022`} {item instanceof Attribute && } {item instanceof Predicate && ( { onChangeValue(data.schema, item.label || name, name, value) diff --git a/packages/legacy/core/App/utils/helpers.ts b/packages/legacy/core/App/utils/helpers.ts index 0998641126..e44216f95c 100644 --- a/packages/legacy/core/App/utils/helpers.ts +++ b/packages/legacy/core/App/utils/helpers.ts @@ -23,6 +23,7 @@ import { ProofFormatDataMessagePayload, } from '@aries-framework/core/build/modules/proofs/protocol/ProofProtocolOptions' import { useConnectionById } from '@aries-framework/react-hooks' +import { CaptureBaseAttributeType } from '@hyperledger/aries-oca' import { Attribute, Predicate, @@ -32,7 +33,8 @@ import { import { Buffer } from 'buffer' import moment from 'moment' import { ParsedUrl, parseUrl } from 'query-string' -import { Dispatch, ReactNode, SetStateAction } from 'react' +import { ReactNode } from 'react' +import { TFunction } from 'react-i18next' import { domain } from '../constants' import { i18n } from '../localization/index' @@ -208,11 +210,7 @@ export function formatTime( return formattedTime } -export function formatIfDate( - format: string | undefined, - value: string | number | null, - setter: Dispatch> -) { +export function formatIfDate(format: string | undefined, value: string | number | null) { const potentialDate = value ? value.toString() : null if (format === 'YYYYMMDD' && potentialDate && potentialDate.length === format.length) { const year = potentialDate.substring(0, 4) @@ -221,9 +219,10 @@ export function formatIfDate( // NOTE: JavaScript counts months from 0 to 11: January = 0, December = 11. const date = new Date(Number(year), Number(month) - 1, Number(day)) if (!isNaN(date.getDate())) { - setter(formatTime(date, { shortMonth: true })) + return formatTime(date, { shortMonth: true }) } } + return value } /** @@ -388,6 +387,7 @@ export const processProofAttributes = ( } processedAttributes[credName].attributes?.push( new Attribute({ + ...requestedProofAttributes[key], revoked, credentialId: credential?.credentialId, name: attributeName, @@ -406,7 +406,6 @@ export const processProofPredicates = ( credentialRecords?: CredentialExchangeRecord[] ): { [key: string]: ProofCredentialPredicates } => { const processedPredicates = {} as { [key: string]: ProofCredentialPredicates } - const requestedProofPredicates = request?.anoncreds?.requested_predicates ?? request?.indy?.requested_predicates const retrievedCredentialPredicates = credentials?.proofFormats?.anoncreds?.predicates ?? credentials?.proofFormats?.indy?.predicates @@ -452,6 +451,7 @@ export const processProofPredicates = ( processedPredicates[credName].predicates?.push( new Predicate({ + ...requestedProofPredicates[key], credentialId, name, revoked, @@ -479,6 +479,37 @@ export const mergeAttributesAndPredicates = ( return merged } +export const pTypeToText = ( + item: Predicate, + t: TFunction<'translation', undefined>, + attributeTypes?: Record +) => { + const itemCopy = { ...item } + const pTypeMap: { [key: string]: string | undefined } = { + '>=': t('ProofRequest.PredicateGe'), + '>': t('ProofRequest.PredicateGr'), + '<=': t('ProofRequest.PredicateLe'), + '<': t('ProofRequest.PredicateLs'), + } + const pTypeDateMap: { [key: string]: string | undefined } = { + '>=': t('ProofRequest.PredicateGeDate'), + '>': t('ProofRequest.PredicateGeDate'), + '<=': t('ProofRequest.PredicateLeDate'), + '<': t('ProofRequest.PredicateLeDate'), + } + const pTypeDateOffset: { [key: string]: number | undefined } = { + '>=': -1, + '<=': 1, + } + if (attributeTypes && attributeTypes[item.name ?? ''] == CaptureBaseAttributeType.DateTime) { + itemCopy.pType = pTypeDateMap[item.pType] ?? item.pType + itemCopy.pValue = parseInt(`${itemCopy.pValue}`) + (pTypeDateOffset[item.pType] ?? 0) + } else { + itemCopy.pType = pTypeMap[item.pType] ?? item.pType + } + return itemCopy +} + /** * @deprecated The function should not be used */ diff --git a/packages/legacy/core/__tests__/screens/ProofRequest.test.tsx b/packages/legacy/core/__tests__/screens/ProofRequest.test.tsx index 3305ff032a..2e2c938723 100644 --- a/packages/legacy/core/__tests__/screens/ProofRequest.test.tsx +++ b/packages/legacy/core/__tests__/screens/ProofRequest.test.tsx @@ -15,6 +15,7 @@ import { testIdWithKey } from '../../App/utils/testable' import configurationContext from '../contexts/configuration' import networkContext from '../contexts/network' import timeTravel from '../helpers/timetravel' +import { useTranslation } from '../../__mocks__/react-i18next' jest.mock('react-native/Libraries/EventEmitter/NativeEventEmitter') jest.mock('@react-native-community/netinfo', () => mockRNCNetInfo) @@ -58,6 +59,8 @@ describe('displays a proof request screen', () => { const testTime = '2022-02-11 20:00:18.180718' const testAge = '16' + const { t } = useTranslation() + const { id: credentialId } = new CredentialExchangeRecord({ threadId: '1', state: CredentialState.Done, @@ -367,7 +370,7 @@ describe('displays a proof request screen', () => { const emailLabel = getByText(/Email/, { exact: false }) const emailValue = getByText(testEmail) const ageLabel = getByText(/Age/, { exact: false }) - const ageValue = getByText('<= 18') + const ageValue = getByText(t('ProofRequest.PredicateLe') + ' 18') const ageNotSatisfied = getByText('ProofRequest.PredicateNotSatisfied', { exact: false }) const shareButton = getByTestId(testIdWithKey('Share')) const declineButton = getByTestId(testIdWithKey('Decline')) diff --git a/packages/legacy/core/__tests__/screens/__snapshots__/ProofDetails.test.tsx.snap b/packages/legacy/core/__tests__/screens/__snapshots__/ProofDetails.test.tsx.snap index d60e36d0eb..adcf26f56a 100644 --- a/packages/legacy/core/__tests__/screens/__snapshots__/ProofDetails.test.tsx.snap +++ b/packages/legacy/core/__tests__/screens/__snapshots__/ProofDetails.test.tsx.snap @@ -328,14 +328,17 @@ exports[`ProofDetails Component with a verified proof record renders correctly w Object { "paddingBottom": 10, "paddingTop": 20, + "width": "65%", } } > @@ -225,7 +226,6 @@ exports[`ProofRequestDetails Component Renders correctly 1`] = ` "fontSize": 18, "fontWeight": "bold", "marginRight": 8, - "paddingVertical": 8, } } > @@ -245,7 +245,6 @@ exports[`ProofRequestDetails Component Renders correctly 1`] = ` "fontSize": 18, "fontWeight": "bold", "marginRight": 8, - "paddingVertical": 8, } } > @@ -258,7 +257,6 @@ exports[`ProofRequestDetails Component Renders correctly 1`] = ` "fontSize": 18, "fontWeight": "bold", "marginRight": 8, - "paddingVertical": 8, } } /> @@ -274,6 +272,7 @@ exports[`ProofRequestDetails Component Renders correctly 1`] = ` style={ Object { "flexDirection": "row", + "paddingVertical": 8, } } > @@ -284,7 +283,6 @@ exports[`ProofRequestDetails Component Renders correctly 1`] = ` "fontSize": 18, "fontWeight": "bold", "marginRight": 8, - "paddingVertical": 8, } } > @@ -304,7 +302,6 @@ exports[`ProofRequestDetails Component Renders correctly 1`] = ` "fontSize": 18, "fontWeight": "bold", "marginRight": 8, - "paddingVertical": 8, } } > @@ -317,7 +314,6 @@ exports[`ProofRequestDetails Component Renders correctly 1`] = ` "fontSize": 18, "fontWeight": "bold", "marginRight": 8, - "paddingVertical": 8, } } /> diff --git a/packages/legacy/core/__tests__/utils/helpers.test.ts b/packages/legacy/core/__tests__/utils/helpers.test.ts index 6038331fa3..34f89ce71f 100644 --- a/packages/legacy/core/__tests__/utils/helpers.test.ts +++ b/packages/legacy/core/__tests__/utils/helpers.test.ts @@ -97,39 +97,34 @@ describe('formatTime', () => { }) describe('formatIfDate', () => { - let setter = jest.fn() - - beforeEach(() => { - setter = jest.fn() - }) afterEach(() => { jest.clearAllMocks() }) test('without format', () => { - formatIfDate(undefined, '20020523', setter) - expect(setter).toBeCalledTimes(0) + const result = formatIfDate(undefined, '20020523') + expect(result).toEqual('20020523') }) test('with format and string date', () => { - formatIfDate('YYYYMMDD', '20020523', setter) - expect(setter).toBeCalledTimes(1) + const result = formatIfDate('YYYYMMDD', '20020523') + expect(result).toEqual("May 23, 2002") }) test('with format and number date', () => { - formatIfDate('YYYYMMDD', 20020523, setter) - expect(setter).toBeCalledTimes(1) + const result = formatIfDate('YYYYMMDD', 20020523) + expect(result).toEqual("May 23, 2002") }) test('with format but invalid string date', () => { - formatIfDate('YYYYMMDD', '203', setter) - expect(setter).toBeCalledTimes(0) + const result = formatIfDate('YYYYMMDD', '203') + expect(result).toEqual('203') }) test('with format but invalid number date', () => { - formatIfDate('YYYYMMDD', 203, setter) - expect(setter).toBeCalledTimes(0) + const result = formatIfDate('YYYYMMDD', 203) + expect(result).toEqual(203) }) })