Skip to content

Commit

Permalink
feat: Add support for selecting credential from multiple OID4VC from …
Browse files Browse the repository at this point in the history
…list

Signed-off-by: tusharbhayani <[email protected]>
  • Loading branch information
tusharbhayani committed Dec 23, 2024
1 parent 786e54a commit a5e09ba
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 33 deletions.
22 changes: 16 additions & 6 deletions app/components/OpenId/CredentialRowCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native'
import { Image, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native'

import { useTheme } from '../../contexts/theme'

Expand All @@ -7,14 +7,19 @@ interface CredentialRowCardProps {
issuer?: string
onPress?(): void
bgColor?: string
bgImage?: string
txtColor?: string
hideBorder?: boolean
showFullText?: boolean
}

export function OpenIDCredentialRowCard({ name, issuer, bgColor, onPress }: CredentialRowCardProps) {
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: {
Expand All @@ -26,23 +31,28 @@ export function OpenIDCredentialRowCard({ name, issuer, bgColor, onPress }: Cred
},
issuerBadge: {
borderRadius: 8,
width: '30%',
width: badgeHeight,
height: badgeHeight,
backgroundColor: 'red',
marginRight: 10,
overflow: 'hidden',
},
infoContainer: {
flex: 1,
justifyContent: 'space-between',
},
imageStyle: { width: badgeWidth, height: badgeHeight, borderRadius: 8 },
})
//
return (
<View style={style.container}>
<TouchableOpacity onPress={onPress} style={style.rowContainer}>
<View style={[style.issuerBadge, bgColor ? { backgroundColor: bgColor } : {}]} />
<View style={[style.issuerBadge, bgColor ? { backgroundColor: bgColor } : {}]}>
{bgImage ? <Image style={style.imageStyle} source={{ uri: bgImage }} resizeMode="cover" /> : null}
</View>
<View style={[style.infoContainer, issuer ? { justifyContent: 'center' } : {}]}>
<Text style={TextTheme.title}>{name}</Text>
{issuer && <Text style={TextTheme.labelSubtitle}>{issuer}</Text>}
<Text style={[TextTheme.title, txtColor ? { color: txtColor } : {}]}>{name}</Text>
{issuer && <Text style={[TextTheme.labelSubtitle, txtColor ? { color: txtColor } : {}]}>{issuer}</Text>}
</View>
</TouchableOpacity>
</View>
Expand Down
111 changes: 86 additions & 25 deletions app/components/OpenId/OpenIDProofPresentation.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { formatDifPexCredentialsForRequest, sanitizeString, shareProof, useAdeyaAgent } from '@adeya/ssi'
import {
ClaimFormat,
CredentialMetadata,
DisplayImage,
formatDifPexCredentialsForRequest,
sanitizeString,
shareProof,
useAdeyaAgent,
} from '@adeya/ssi'
import { StackScreenProps } from '@react-navigation/stack'
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DeviceEventEmitter, ScrollView, StyleSheet, Text, View } from 'react-native'
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, TabStacks } from '../../types/navigators'
import { NotificationStackParams, Screens, Stacks, TabStacks } from '../../types/navigators'
import { ModalUsage } from '../../types/remove'
import { testIdWithKey } from '../../utils/testable'
import Button, { ButtonType } from '../buttons/Button'
Expand Down Expand Up @@ -40,6 +49,11 @@ const styles = StyleSheet.create({
footerButton: {
paddingTop: 10,
},
credActionText: {
fontSize: 16,
fontWeight: 'bold',
textDecorationLine: 'underline',
},
})

const OpenIDProofPresentation: React.FC<OpenIDProofPresentationProps> = ({
Expand All @@ -51,6 +65,7 @@ const OpenIDProofPresentation: React.FC<OpenIDProofPresentationProps> = ({
const [declineModalVisible, setDeclineModalVisible] = useState(false)
const [buttonsVisible, setButtonsVisible] = useState(true)
const [acceptModalVisible, setAcceptModalVisible] = useState(false)
const [selectedCredId, setSelectedCredId] = useState<string | null>(null)

const { ColorPallet } = useTheme()
const { t } = useTranslation()
Expand All @@ -66,21 +81,23 @@ const OpenIDProofPresentation: React.FC<OpenIDProofPresentationProps> = ({
[credential],
)

const selectedCredentials:
| {
[inputDescriptorId: string]: string
}
| undefined = useMemo(
() =>
submission?.entries.reduce((acc, entry) => {
if (entry.isSatisfied) {
//TODO: Support multiplae credentials
return { ...acc, [entry.inputDescriptorId]: entry.credentials[0].id }
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
}, {}),
[submission],
)
}

return acc
}, {}) // Default empty object for accumulator
}, [submission, selectedCredId])

useEffect(() => {}, [selectedCredentials])

const { verifierName } = useMemo(() => {
return { verifierName: credential?.verifierHostName }
Expand Down Expand Up @@ -124,21 +141,51 @@ const OpenIDProofPresentation: React.FC<OpenIDProofPresentationProps> = ({
)
}

const onCredChange = (credId: string) => {
setSelectedCredId(credId)
}
const handleAltCredChange = (
selectedCred: {
id: string
credentialName: string
issuerName?: string
requestedAttributes?: string[]
disclosedPayload?: Record<string, unknown>
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 (
<View style={styles.credentialsList}>
{submission.entries.map((s, i) => {
{submission.entries.map((credential, index) => {
//TODO: Support multiple credentials
const selectedCredential = s.credentials[0]

const selectedCredential = credential.credentials[0]
return (
<View key={i}>
<OpenIDCredentialRowCard name={s.name} issuer={verifierName} onPress={() => {}} />
{s.isSatisfied && selectedCredential?.requestedAttributes ? (
<View key={index}>
<OpenIDCredentialRowCard
name={credential.name}
bgImage={selectedCredential.backgroundImage?.url}
issuer={verifierName}
onPress={() => {}}
/>
{credential.isSatisfied && selectedCredential?.requestedAttributes ? (
<View style={{ marginTop: 16, gap: 8 }}>
{s.description && <Text style={TextTheme.labelSubtitle}>{s.description}</Text>}
{credential.description && <Text style={TextTheme.labelSubtitle}>{credential.description}</Text>}
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
{selectedCredential.requestedAttributes.map(a => (
<View key={a} style={{ flexBasis: '50%' }}>
Expand All @@ -150,6 +197,20 @@ const OpenIDProofPresentation: React.FC<OpenIDProofPresentationProps> = ({
) : (
<Text style={TextTheme.title}>This credential is not present in your wallet.</Text>
)}
{credential.credentials.length > 1 && (
<TouchableOpacity
onPress={() => {
handleAltCredChange(credential.credentials, credential.inputDescriptorId)
}}
testID={testIdWithKey('changeCredential')}
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', marginTop: 40 }}>
<Text style={styles.credActionText}>{t('ProofRequest.ChangeCredential')}</Text>
<Icon
style={{ ...styles.credActionText, fontSize: styles.credActionText.fontSize + 5 }}
name="chevron-right"
/>
</TouchableOpacity>
)}
</View>
)
})}
Expand Down
3 changes: 1 addition & 2 deletions app/components/misc/CredentialCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { ViewStyle } from 'react-native'

import { useConfiguration } from '../../contexts/configuration'
import { useTheme } from '../../contexts/theme'
import { GenericFn } from '../../types/fn'
import OpenIdCredentialCard from '../OpenId/OpenIDCredentialCard'

import CredentialCard10 from './CredentialCard10'
Expand All @@ -21,7 +20,7 @@ interface CredentialCardProps {
credDefId?: string
schemaId?: string
credName?: string
onPress?: GenericFn
onPress?: () => void
style?: ViewStyle
proof?: boolean
displayItems?: (Attribute | Predicate)[]
Expand Down
6 changes: 6 additions & 0 deletions app/navigators/ProofRequestStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import HeaderButton, { ButtonLocation } from '../components/buttons/HeaderButton
import HeaderRightHome from '../components/buttons/HeaderHome'
import { useTheme } from '../contexts/theme'
import ListProofRequests from '../screens/ListProofRequests'
import OpenID4VCProofChangeCredential from '../screens/OpenID4VCProofChangeCredential'
import ProofChangeCredential from '../screens/ProofChangeCredential'
import ProofChangeCredentialW3C from '../screens/ProofChangeCredentialW3C'
import ProofDetails from '../screens/ProofDetails'
Expand Down Expand Up @@ -47,6 +48,11 @@ const ProofRequestStack: React.FC = () => {
component={ProofChangeCredentialW3C}
options={{ title: t('Screens.ProofChangeCredentialW3C') }}
/>
<Stack.Screen
name={Screens.ProofChangeCredentialOpenId4VP}
component={OpenID4VCProofChangeCredential}
options={{ title: t('Screens.ProofChangeCredentialW3C') }}
/>
<Stack.Screen
name={Screens.ProofRequesting}
component={ProofRequesting}
Expand Down
101 changes: 101 additions & 0 deletions app/screens/OpenID4VCProofChangeCredential.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { StackScreenProps } from '@react-navigation/stack'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, StyleSheet, Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'

import { CredentialCard } from '../components/misc'
import { useTheme } from '../contexts/theme'
import { ProofRequestsStackParams, Screens } from '../types/navigators'
import { testIdWithKey } from '../utils/testable'

type ProofChangeProps = StackScreenProps<ProofRequestsStackParams, Screens.ProofChangeCredentialOpenId4VP>

const OpenID4VCProofChangeCredential: React.FC<ProofChangeProps> = ({ route, navigation }) => {
if (!route?.params) {
throw new Error('Change credential route params were not set properly')
}
const { selectedCred, onCredChange } = route.params

const { ColorPallet, TextTheme } = useTheme()
const { t } = useTranslation()
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,
},
})

const listHeader = () => {
return (
<View style={{ ...styles.pageMargin, marginVertical: 20 }}>
<Text style={TextTheme.normal}>{t('ProofRequest.MultipleCredentials')}</Text>
</View>
)
}

const changeCred = (credId: string) => {
onCredChange(credId)
navigation.goBack()
}

const renderCredential = ({ item }: { item: any }) => {
const displayItems = [
{ label: 'Issuer', value: item?.issuerName },
...Object.entries(item?.disclosedPayload || {}).map(([key, value]) => ({
label: key.charAt(0).toUpperCase() + key.slice(1), // Capitalize key
value: value?.toString() ?? '',
})),
]

return (
<View style={styles.pageMargin}>
<View
testID={testIdWithKey(`select:${item?.id}`)}
style={[item?.id === selectedCred ? styles.selectedCred : {}, { marginBottom: 10 }]}>
<CredentialCard
credential={item?.credExchangeRecord}
credDefId={item?.credDefId}
schemaId={item?.schemaId}
displayItems={displayItems}
credName={item?.credentialName}
existsInWallet={true}
satisfiedPredicates={true}
proof={true}
onPress={() => {
changeCred(item?.id)
}}
/>
</View>
</View>
)
}

return (
<SafeAreaView style={styles.pageContainer} edges={['bottom', 'left', 'right']}>
<FlatList
data={selectedCred}
ListHeaderComponent={listHeader}
renderItem={renderCredential}
keyExtractor={item => item?.id ?? ''}
/>
</SafeAreaView>
)
}

export default OpenID4VCProofChangeCredential
7 changes: 7 additions & 0 deletions app/types/navigators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export enum Screens {
CredentialDetailsW3C = 'Credential Details W3C',
ProofChangeCredential = 'Choose a credential',
ProofChangeCredentialW3C = 'Choose a W3C credential',
ProofChangeCredentialOpenId4VP = 'Choose a OpenId4VP credential',
DataRetention = 'Data Retention',
Explore = 'Explore',
OrganizationDetails = 'Organization Details',
Expand Down Expand Up @@ -150,6 +151,12 @@ export type ProofRequestsStackParams = {
proofId: string
onCredChange: (arg: string) => void
}
[Screens.ProofChangeCredentialOpenId4VP]: {
selectedCred: string
altCredentials: string[]
proofId: string
onCredChange: (arg: string) => void
}
}

export type CredentialStackParams = {
Expand Down

0 comments on commit a5e09ba

Please sign in to comment.