diff --git a/App.tsx b/App.tsx index 9073981f..305da138 100644 --- a/App.tsx +++ b/App.tsx @@ -11,6 +11,7 @@ import SplashScreen from 'react-native-splash-screen' import Toast from 'react-native-toast-message' import { animatedComponents } from './app/animated-components' +import { OpenIDCredentialRecordProvider } from './app/components/Provider/OpenIDCredentialRecordProvider' import PushNotifications from './app/components/PushNotifications' import ErrorModal from './app/components/modals/ErrorModal' import NetInfo from './app/components/network/NetInfo' @@ -54,31 +55,33 @@ const App = () => { return ( - - - - - - - - - - - - + + + + + + + + + + + + + + ) diff --git a/Gemfile b/Gemfile index 1fa2c2e1..b550fb64 100644 --- a/Gemfile +++ b/Gemfile @@ -3,4 +3,7 @@ source 'https://rubygems.org' # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version ruby ">= 2.6.10" -gem 'cocoapods', '~> 1.12' +# Cocoapods 1.15 introduced a bug which break the build. We will remove the upper +# bound in the template on Cocoapods with next React Native release. +gem 'cocoapods', '>= 1.13', '< 1.15' +gem 'activesupport', '>= 6.1.7.3', '< 7.1.0' diff --git a/app/components/OpenId/CredentialRowCard.tsx b/app/components/OpenId/CredentialRowCard.tsx new file mode 100644 index 00000000..71fa3829 --- /dev/null +++ b/app/components/OpenId/CredentialRowCard.tsx @@ -0,0 +1,60 @@ +import { Image, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native' + +import { useTheme } from '../../contexts/theme' + +interface CredentialRowCardProps { + name: string + issuer?: string + onPress?(): void + bgColor?: string + bgImage?: string + txtColor?: string + hideBorder?: boolean + showFullText?: boolean +} + +export function OpenIDCredentialRowCard({ name, issuer, bgColor, bgImage, txtColor, onPress }: CredentialRowCardProps) { + const { TextTheme } = useTheme() + const { width } = useWindowDimensions() + + const badgeWidth = 0.4 * width + const badgeHeight = 0.6 * badgeWidth + + const style = StyleSheet.create({ + container: {}, + rowContainer: { + flexDirection: 'row', + borderRadius: 8, + // backgroundColor: '#202020', + padding: 5, + minHeight: 0.2 * width, + }, + issuerBadge: { + borderRadius: 8, + width: badgeHeight, + height: badgeHeight, + backgroundColor: 'red', + marginRight: 10, + overflow: 'hidden', + }, + infoContainer: { + flex: 1, + justifyContent: 'space-between', + }, + imageStyle: { width: badgeWidth, height: badgeHeight, borderRadius: 8 }, + }) + // + return ( + + + + {bgImage ? : null} + + + {name} + {issuer && {issuer}} + + + + ) +} diff --git a/app/components/OpenId/OpenIDCredentialCard.tsx b/app/components/OpenId/OpenIDCredentialCard.tsx new file mode 100644 index 00000000..2d719773 --- /dev/null +++ b/app/components/OpenId/OpenIDCredentialCard.tsx @@ -0,0 +1,164 @@ +import { + ClaimFormat, + GenericCredentialExchangeRecord, + getOpenId4VcCredentialMetadata, + getW3cCredentialDisplay, + getW3cIssuerDisplay, + JsonTransformer, + W3cCredentialJson, + W3cCredentialRecord, +} from '@adeya/ssi' +import React from 'react' +import { Image, ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react-native' + +import { ColorPallet } from '../../theme' + +type OpenIdCredentialCardProps = { + credentialRecord: GenericCredentialExchangeRecord + onPress?(): void + textColor?: string + shadow?: boolean +} + +export function getTextColorBasedOnBg(bgColor: string) { + return Number.parseInt(bgColor.replace('#', ''), 16) > 0xffffff / 2 ? '#212529' : '#f6f9fc' +} + +const OpenIdCredentialCard: React.FC = ({ credentialRecord, textColor, onPress }) => { + const credential = JsonTransformer.toJSON( + (credentialRecord as W3cCredentialRecord).credential.claimFormat === ClaimFormat.JwtVc + ? credentialRecord?.credential?.credential + : credentialRecord?.credential, + ) as W3cCredentialJson + const openId4VcMetadata = getOpenId4VcCredentialMetadata(credentialRecord as W3cCredentialRecord) + const issuerShow = getW3cIssuerDisplay(credential, openId4VcMetadata) + const credentialShow = getW3cCredentialDisplay(credential, openId4VcMetadata) + + const styles = StyleSheet.create({ + container: { + borderRadius: 8, + position: 'relative', + }, + card: { + width: '100%', + borderRadius: 8, + height: 164, + overflow: 'hidden', + }, + backgroundView: { + width: '100%', + borderRadius: 8, + height: '100%', + }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingBottom: 8, + }, + iconContainer: { + paddingRight: 16, + }, + textContainer: { + flex: 1, + alignItems: 'flex-end', + }, + heading: { + fontSize: 16, + textAlign: 'right', + }, + subtitle: { + fontSize: 12, + textAlign: 'right', + opacity: 0.8, + }, + cardFooter: { + paddingTop: 8, + }, + footerTextContainer: { + alignItems: 'flex-start', + }, + issuerLabel: { + fontSize: 10, + opacity: 0.8, + }, + issuerName: { + fontSize: 12, + }, + cardBackground: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + width: '100%', + height: '100%', + }, + backgroundImage: { + width: '100%', + height: '100%', + }, + backgroundFallback: { + width: '100%', + height: '100%', + }, + cardContainer: { + flex: 1, + padding: 16, + justifyContent: 'space-between', + }, + }) + + textColor = textColor ? textColor : getTextColorBasedOnBg(ColorPallet.brand.primary ?? '#000') + + return ( + + + + + + + {issuerShow?.logo?.url ? ( + {issuerShow.logo.altText} + ) : null} + + + + {credentialShow.name} + + + {credentialShow.description} + + + + + + Issuer + + {issuerShow.name} + + + + + + + + ) +} + +export default OpenIdCredentialCard diff --git a/app/components/OpenId/OpenIDProofPresentation.tsx b/app/components/OpenId/OpenIDProofPresentation.tsx new file mode 100644 index 00000000..7baa0f0d --- /dev/null +++ b/app/components/OpenId/OpenIDProofPresentation.tsx @@ -0,0 +1,291 @@ +import { + ClaimFormat, + CredentialMetadata, + DisplayImage, + formatDifPexCredentialsForRequest, + sanitizeString, + shareProof, + useAdeyaAgent, +} from '@adeya/ssi' +import { StackScreenProps } from '@react-navigation/stack' +import { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { DeviceEventEmitter, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' +import Icon from 'react-native-vector-icons/MaterialIcons' + +import { EventTypes } from '../../constants' +import { useTheme } from '../../contexts/theme' +import ProofRequestAccept from '../../screens/ProofRequestAccept' +import { ListItems, TextTheme } from '../../theme' +import { BifoldError } from '../../types/error' +import { NotificationStackParams, Screens, Stacks, TabStacks } from '../../types/navigators' +import { ModalUsage } from '../../types/remove' +import { testIdWithKey } from '../../utils/testable' +import Button, { ButtonType } from '../buttons/Button' +import CommonRemoveModal from '../modals/CommonRemoveModal' + +import { OpenIDCredentialRowCard } from './CredentialRowCard' + +type OpenIDProofPresentationProps = StackScreenProps + +const styles = StyleSheet.create({ + pageContent: { + flexGrow: 1, + justifyContent: 'space-between', + padding: 10, + }, + credentialsList: { + marginTop: 20, + justifyContent: 'space-between', + }, + headerTextContainer: { + paddingVertical: 16, + }, + headerText: { + ...ListItems.recordAttributeText, + flexShrink: 1, + }, + footerButton: { + paddingTop: 10, + }, + credActionText: { + fontSize: 16, + fontWeight: 'bold', + textDecorationLine: 'underline', + }, +}) + +const OpenIDProofPresentation: React.FC = ({ + navigation, + route: { + params: { credential }, + }, +}: OpenIDProofPresentationProps) => { + const [declineModalVisible, setDeclineModalVisible] = useState(false) + const [buttonsVisible, setButtonsVisible] = useState(true) + const [acceptModalVisible, setAcceptModalVisible] = useState(false) + const [selectedCredId, setSelectedCredId] = useState(null) + + const { ColorPallet } = useTheme() + const { t } = useTranslation() + const { agent } = useAdeyaAgent() + + const toggleDeclineModalVisible = () => setDeclineModalVisible(!declineModalVisible) + + const submission = useMemo( + () => + credential && credential.credentialsForRequest + ? formatDifPexCredentialsForRequest(credential.credentialsForRequest) + : undefined, + [credential], + ) + + const selectedCredentials = useMemo(() => { + return submission?.entries.reduce((acc, entry) => { + // Check if the entry is satisfied and has credentials + if (entry.isSatisfied) { + // Iterate through the credentials for the entry + const selectedCredential = entry.credentials.find(item => item.id === selectedCredId) + if (selectedCredential) { + // If found, add it to the accumulator with the appropriate inputDescriptorId + return { ...acc, [entry.inputDescriptorId]: selectedCredential.id } + } + } + + return acc + }, {}) // Default empty object for accumulator + }, [submission, selectedCredId]) + + useEffect(() => {}, [selectedCredentials]) + + const { verifierName } = useMemo(() => { + return { verifierName: credential?.verifierHostName } + }, [credential]) + + const handleAcceptTouched = async () => { + try { + if (!agent || !credential.credentialsForRequest || !selectedCredentials) { + return + } + await shareProof({ + agent, + authorizationRequest: credential.authorizationRequest, + credentialsForRequest: credential.credentialsForRequest, + selectedCredentials, + }) + + setAcceptModalVisible(true) + } catch (err: unknown) { + setButtonsVisible(true) + const error = new BifoldError(t('Error.Title1027'), t('Error.Message1027'), (err as Error)?.message ?? err, 1027) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, error) + } + } + + const handleDeclineTouched = async () => { + toggleDeclineModalVisible() + navigation.getParent()?.navigate(TabStacks.HomeStack, { screen: Screens.Home }) + } + + const renderHeader = () => { + return ( + + + + You have received an information request + {verifierName ? ` from ${verifierName}` : ''}. + + + + ) + } + + const onCredChange = (credId: string) => { + setSelectedCredId(credId) + } + const handleAltCredChange = ( + selectedCred: { + id: string + credentialName: string + issuerName?: string + requestedAttributes?: string[] + disclosedPayload?: Record + metadata?: CredentialMetadata + backgroundColor?: string + backgroundImage?: DisplayImage + claimFormat: ClaimFormat | undefined | 'AnonCreds' + }[], + proofId: string, + ) => { + navigation.getParent()?.navigate(Stacks.ProofRequestsStack, { + screen: Screens.ProofChangeCredentialOpenId4VP, + params: { + selectedCred, + proofId, + onCredChange, + }, + }) + } + const renderBody = () => { + if (!submission) return null + + return ( + + {submission.entries.map((credential, index) => { + //TODO: Support multiple credentials + const selectedCredential = credential.credentials[0] + return ( + + {}} + /> + {credential.isSatisfied && selectedCredential?.requestedAttributes ? ( + + {credential.description && {credential.description}} + + {selectedCredential.requestedAttributes.map(attribute => ( + + • {sanitizeString(attribute)} + + ))} + + + ) : ( + This credential is not present in your wallet. + )} + {credential.credentials.length > 1 && ( + { + handleAltCredChange(credential.credentials, credential.inputDescriptorId) + }} + testID={testIdWithKey('changeCredential')} + style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginTop: 40 }}> + {t('ProofRequest.ChangeCredential')} + + + )} + + ) + })} + + ) + } + + const footerButton = ( + title: string, + buttonPress: () => void, + buttonType: ButtonType, + testID: string, + accessibilityLabel: string, + ) => { + return ( + +