Skip to content

Commit

Permalink
feat: Implement UI for displaying OpenID4VCI credential offer (openwa…
Browse files Browse the repository at this point in the history
…llet-foundation#1236)

Signed-off-by: Mostafa Gamal <[email protected]>
  • Loading branch information
MosCD3 authored Sep 20, 2024
1 parent 3d18606 commit 6c79171
Show file tree
Hide file tree
Showing 18 changed files with 936 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const HomeFooterView: React.FC<HomeFooterViewProps> = ({ 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({
Expand Down
15 changes: 4 additions & 11 deletions packages/legacy/core/App/container-api.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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'

Expand Down Expand Up @@ -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<NotificationListItemProps>
Expand Down
37 changes: 29 additions & 8 deletions packages/legacy/core/App/hooks/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<BasicMessageRecord | CredentialRecord | ProofExchangeRecord> => {
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<NotificationReturnType>([])

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
Expand Down Expand Up @@ -58,11 +69,21 @@ export const useNotifications = (): Array<BasicMessageRecord | CredentialRecord
}
})

const notif = [...messagesToShow, ...offers, ...proofsRequested, ...validProofsDone, ...revoked].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)
const openIDCreds: Array<SdJwtVcRecord | W3cCredentialRecord> = []
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
}
Original file line number Diff line number Diff line change
@@ -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<CredentialCard10Props> = ({ 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 <View/>
}
if (typeof src === 'string' && src.endsWith('.svg'))
return <SvgUri role="img" width={width} height={height} uri={src} aria-label={logo.altText} />

return (
<Image
source={toImageSource(display.issuer.logo)}
style={{
flex: 4,
resizeMode: 'contain',
width: width,
height: height
}}
/>
)
}


const CardHeader: React.FC = () => {
return (
<View style={[styles.outerHeaderContainer]}>
<View testID={testIdWithKey('CredentialCardHeader')} style={[styles.innerHeaderContainer]}>
{logoContaineter(display.issuer.logo)}
<View>
<View>
<Text
numberOfLines={1}
ellipsizeMode="tail"
style={[
TextTheme.label,
{
color:
display.textColor ??
credentialTextColor(
ColorPallet,
display.backgroundColor
),
textAlignVertical: 'center',
},
]}
testID={testIdWithKey('CredentialIssuer')}
maxFontSizeMultiplier={1}
>
{display.name}
</Text>
</View>
<View>
<Text
numberOfLines={1}
ellipsizeMode="tail"
style={[
TextTheme.label,
{
color:
display.textColor ??
credentialTextColor(
ColorPallet,
display.backgroundColor
),
textAlign: 'right',
textAlignVertical: 'center',
},
]}
testID={testIdWithKey('CredentialName')}
maxFontSizeMultiplier={1}
>
{display.description}
</Text>
</View>
</View>
</View>
</View>
)
}

const CardBody: React.FC = () => {
return <View style={styles.bodyContainer} testID={testIdWithKey('CredentialCardBody')}></View>
}

const CardFooter: React.FC = () => {
return (
<View testID={testIdWithKey('CredentialCardFooter')} style={styles.footerContainer}>
<Text
style={[
TextTheme.caption,
{
color:
display.textColor ??
credentialTextColor(
ColorPallet,
display.backgroundColor
),
},
]}
testID={testIdWithKey('CredentialIssued')}
maxFontSizeMultiplier={1}
>
{t('CredentialDetails.Issued')}: {formatTime(new Date(), { shortMonth: true })}
</Text>
</View>
)
}
const CredentialCard: React.FC = () => {
return (
<>
<CardHeader />
<CardBody />
<CardFooter />
</>
)
}

return (
<TouchableOpacity
accessible={true}
accessibilityLabel={`${
display.issuer.name ? `${t('Credentials.IssuedBy')} ${display.issuer.name}` : ''
}, ${t('Credentials.Credential')}.`}
disabled={typeof onPress === 'undefined' ? true : false}
onPress={onPress}
style={[styles.container, style]}
testID={testIdWithKey('ShowCredentialDetails')}
>
<View style={[styles.flexGrow, { overflow: 'hidden' }]} testID={testIdWithKey('CredentialCard')}>
{display.backgroundImage ? (
<ImageBackground
source={toImageSource(display.backgroundImage)}
style={styles.flexGrow}
imageStyle={{ borderRadius }}
>
<CredentialCard/>
</ImageBackground>
) : (
<CredentialCard/>
)}
</View>
</TouchableOpacity>
)
}

export default OpenIDCredentialCard
Loading

0 comments on commit 6c79171

Please sign in to comment.