From 26b5d81349a46dedfcc1544996e840bc68da9943 Mon Sep 17 00:00:00 2001 From: tusharbhayani Date: Wed, 20 Nov 2024 13:16:34 +0530 Subject: [PATCH 1/5] feat: Create component for OpenIDC4VCI Signed-off-by: tusharbhayani --- .../OpenId/OpenIDCredentialCard.tsx | 155 ++++++++++++ .../OpenIDCredentialRecordProvider.tsx | 78 ++++++ app/screens/OpenIDCredentialDetails.tsx | 236 ++++++++++++++++++ app/screens/OpenIDCredentialOffer.tsx | 217 ++++++++++++++++ 4 files changed, 686 insertions(+) create mode 100644 app/components/OpenId/OpenIDCredentialCard.tsx create mode 100644 app/components/Provider/OpenIDCredentialRecordProvider.tsx create mode 100644 app/screens/OpenIDCredentialDetails.tsx create mode 100644 app/screens/OpenIDCredentialOffer.tsx diff --git a/app/components/OpenId/OpenIDCredentialCard.tsx b/app/components/OpenId/OpenIDCredentialCard.tsx new file mode 100644 index 00000000..777138e7 --- /dev/null +++ b/app/components/OpenId/OpenIDCredentialCard.tsx @@ -0,0 +1,155 @@ +import { DisplayImage } from '@adeya/ssi' +import React from 'react' +import { Image, ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react-native' + +type OpenIdCredentialCardProps = { + onPress?(): void + name: string + issuerName: string + subtitle?: string + bgColor?: string + textColor?: string + issuerImage?: DisplayImage + backgroundImage?: DisplayImage + shadow?: boolean +} + +export function getTextColorBasedOnBg(bgColor: string) { + return Number.parseInt(bgColor.replace('#', ''), 16) > 0xffffff / 2 ? '#212529' : '#f6f9fc' +} + +const OpenIdCredentialCard: React.FC = ({ + issuerName, + name, + backgroundImage, + issuerImage, + bgColor, + textColor, + onPress, + subtitle, +}) => { + 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(bgColor ?? '#000') + + return ( + + + + + + + {issuerImage?.url ? ( + {issuerImage?.altText} + ) : null} + + + + {name} + + + {subtitle} + + + + + + Issuer + + {issuerName} + + + + + + + + ) +} + +export default OpenIdCredentialCard diff --git a/app/components/Provider/OpenIDCredentialRecordProvider.tsx b/app/components/Provider/OpenIDCredentialRecordProvider.tsx new file mode 100644 index 00000000..89147146 --- /dev/null +++ b/app/components/Provider/OpenIDCredentialRecordProvider.tsx @@ -0,0 +1,78 @@ +import { + addRecord, + defaultState, + filterW3CCredentialsOnly, + isW3CCredentialRecord, + OpenIDCredentialContext, + OpenIDCredentialRecordState, + recordsAddedByType, + recordsRemovedByType, + removeCredential, + removeRecord, + storeOpenIdCredential, + useAdeyaAgent, + W3cCredentialRecord, +} from '@adeya/ssi' +import { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react' + +interface OpenIDCredentialProviderProps { + children: React.ReactNode +} + +const OpenIDCredentialRecordContext = createContext(null as unknown as OpenIDCredentialContext) + +export const OpenIDCredentialRecordProvider: React.FC> = ({ + children, +}: OpenIDCredentialProviderProps) => { + const [state, setState] = useState(defaultState) + + const { agent } = useAdeyaAgent() + + useEffect(() => { + if (!agent) { + return + } + agent.w3cCredentials?.getAllCredentialRecords().then(w3cCredentialRecords => { + setState(prev => ({ + ...prev, + w3cCredentialRecords: filterW3CCredentialsOnly(w3cCredentialRecords), + isLoading: false, + })) + }) + }, [agent]) + + useEffect(() => { + if (!state.isLoading && agent) { + const credentialAdded$ = recordsAddedByType(agent, W3cCredentialRecord).subscribe(record => { + //This handler will return ANY creds added to the wallet even DidComm + //Sounds like a bug in the hooks package + //This check will safe guard the flow untill a fix goes to the hooks + if (isW3CCredentialRecord(record)) { + setState(addRecord(record, state)) + } + }) + + const credentialRemoved$ = recordsRemovedByType(agent, W3cCredentialRecord).subscribe(record => { + setState(removeRecord(record, state)) + }) + + return () => { + credentialAdded$.unsubscribe() + credentialRemoved$.unsubscribe() + } + } + }, [state, agent]) + + return ( + + {children} + + ) +} + +export const useOpenIDCredentials = () => useContext(OpenIDCredentialRecordContext) diff --git a/app/screens/OpenIDCredentialDetails.tsx b/app/screens/OpenIDCredentialDetails.tsx new file mode 100644 index 00000000..81ef0205 --- /dev/null +++ b/app/screens/OpenIDCredentialDetails.tsx @@ -0,0 +1,236 @@ +import { + getCredentialForDisplay, + getOpenId4VcCredentialMetadata, + getW3cCredentialDisplay, + getW3cIssuerDisplay, + W3cCredentialRecord, +} from '@adeya/ssi' +import { StackScreenProps } from '@react-navigation/stack' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { DeviceEventEmitter, FlatList, StyleSheet, Text, View } from 'react-native' +import { Edge, SafeAreaView } from 'react-native-safe-area-context' + +import OpenIdCredentialCard from '../components/OpenId/OpenIDCredentialCard' +import { useOpenIDCredentials } from '../components/Provider/OpenIDCredentialRecordProvider' +import Button, { ButtonType } from '../components/buttons/Button' +import CommonRemoveModal from '../components/modals/CommonRemoveModal' +import RecordField from '../components/record/RecordField' +import RecordFooter from '../components/record/RecordFooter' +import RecordHeader from '../components/record/RecordHeader' +import RecordRemove from '../components/record/RecordRemove' +import { EventTypes, OpenIDCredScreenMode } from '../constants' +import { useTheme } from '../contexts/theme' +import { TextTheme } from '../theme' +import { DeliveryStackParams, Screens, TabStacks } from '../types/navigators' +import { ModalUsage } from '../types/remove' +import { useAppAgent } from '../utils/agent' +import { buildFieldsFromOpenIDTemplate } from '../utils/credential' +import { testIdWithKey } from '../utils/testable' + +import CredentialOfferAccept from './CredentialOfferAccept' + +type OpenIDCredentialDetailsProps = StackScreenProps + +const OpenIDCredentialDetails: React.FC = ({ navigation, route }) => { + // FIXME: change params to accept credential id to avoid 'non-serializable' warnings + const { credential, screenMode } = route.params + const credentialDisplay = getCredentialForDisplay(credential) + const { display, attributes } = credentialDisplay + const fields = buildFieldsFromOpenIDTemplate(attributes) + const { t } = useTranslation() + const { ColorPallet } = useTheme() + const { agent } = useAppAgent() + const { storeOpenIdCredential, removeCredential } = useOpenIDCredentials() + + const [isRemoveModalDisplayed, setIsRemoveModalDisplayed] = 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 = () => setIsRemoveModalDisplayed(!isRemoveModalDisplayed) + + const handleRemove = async () => { + try { + await removeCredential(agent, credential) + navigation.pop() + } catch (err) { + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, err) + } + } + const handleDeclineTouched = async () => { + toggleDeclineModalVisible() + if (screenMode === OpenIDCredScreenMode.offer) + navigation.getParent()?.navigate(TabStacks.HomeStack, { screen: Screens.Home }) + else handleRemove() + } + + const handleAcceptTouched = async () => { + try { + if (!agent) { + return + } + await storeOpenIdCredential(agent, credential) + setAcceptModalVisible(true) + } catch (err: unknown) { + setButtonsVisible(true) + DeviceEventEmitter.emit(EventTypes.ERROR_ADDED, err) + } + } + + const footerButton = ( + title: string, + buttonPress: () => void, + buttonType: ButtonType, + testID: string, + accessibilityLabel: string, + ) => { + return ( + +