Skip to content

Commit

Permalink
feat: history offer connection share proof (#225)
Browse files Browse the repository at this point in the history
Co-authored-by: Sai Ranjit Tummalapalli <[email protected]>
  • Loading branch information
tusharbhayani and sairanjit authored Oct 28, 2024
1 parent b39311c commit 662cf96
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 17 deletions.
3 changes: 3 additions & 0 deletions app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum LocalStorageKeys {
RevokedCredentialsMessageDismissed = 'RevokedCredentialsMessageDismissed',
Preferences = 'PreferencesState',
Tours = 'ToursState',
HistorySettingsOption = 'historySettingsOption',
}

export enum KeychainServices {
Expand Down Expand Up @@ -81,3 +82,5 @@ export const tourMargin = 25
export const hitSlop = { top: 44, bottom: 44, left: 44, right: 44 }

export const CREDENTIAL = 'Credential'

export const CREDENTIAL_W3C = 'Credential_W3C'
16 changes: 16 additions & 0 deletions app/contexts/reducers/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ enum PreferencesDispatchAction {
USE_DEV_VERIFIER_TEMPLATES = 'preferences/useDevVerifierTemplates',
UPDATE_WALLET_NAME = 'preferences/updateWalletName',
USE_DATA_RETENTION = 'preferences/useDataRetention',
HISTORY_CAPABILITY = 'preferences/historyCapability',
}

enum ToursDispatchAction {
Expand Down Expand Up @@ -142,6 +143,21 @@ export const reducer = <S extends State>(state: S, action: ReducerAction<Dispatc

return newState
}
case PreferencesDispatchAction.HISTORY_CAPABILITY: {
const choice = (action?.payload ?? []).pop() ?? false
const preferences = {
...state.preferences,
useHistoryCapability: choice,
}
const newState = {
...state,
preferences,
}

AsyncStorage.setItem(LocalStorageKeys.Preferences, JSON.stringify(preferences))

return newState
}
case PreferencesDispatchAction.USE_DEV_VERIFIER_TEMPLATES: {
const choice = (action?.payload ?? []).pop() ?? false
const preferences = {
Expand Down
1 change: 1 addition & 0 deletions app/contexts/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const defaultState: State = {
useDevVerifierTemplates: false,
walletName: generateRandomWalletName(),
useDataRetention: true,
useHistoryCapability: true,
},
tours: {
seenToursPrompt: false,
Expand Down
28 changes: 28 additions & 0 deletions app/localization/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const translation = {
"Remove": "Remove",
"Yes": "Yes",
"No": "No",
"History": "History",
"SaveSettings": "Save Settings",
},
"Date": {
"ShortFormat": "MMM D",
Expand Down Expand Up @@ -574,6 +576,8 @@ const translation = {
"OrganizationConnection": "Connection",
"RenderCertificate": "Certificate",
"GoogleDriveSignIn": "Google Drive Sign In",
"HistorySettings": "History Settings",
"History": "History",
},
"Loading": {
"TakingTooLong": "This is taking longer than usual. You can return to home or continue waiting.",
Expand Down Expand Up @@ -731,6 +735,30 @@ const translation = {
"RestoreInstructions": "Note: To restore your wallet, you can use a backup from your cloud storage or local device. Please ensure that the Google Drive app is installed on your device and you are signed in.",
"RestoreInstructionsIOS": "If you can't see Google Drive in the file picker, open the Files app --> tap on the three dots at the top --> select 'Edit' --> enable Google Drive.",
},
"History" : {
"SortFilterButton":"Sort/Filter",
"CardTitle":{
"CardAccepted": "You have successfully added a new credential {{Credential}}.",
"CardDeclined": "You have declined the card.",
"CardExpired": "The card has expired.",
"CardRevoked": "The card has been revoked.",
"InformationSent": "Information has been sent to the recipient.",
"WalletPinUpdated": "Your wallet PIN has been successfully updated.",
"ProofRequest": "You have successfully shared your credentials with {{Credential}}.",
"Connection": "You have successfully added a new connection with {{Credential}}.",
},
"CardDescription":{
"CardExpired": "{{cardName}} expired",
"CardRevoked": "{{cardName}} revoked",
"WalletPinUpdated": "New PIN set",
"ProofRequested": "Proof request",
"Connection": "Connection",
},
"Today": "Today",
},
"ActivityHistory": {
"NoHistory": "No History",
}
}

export default translation
5 changes: 4 additions & 1 deletion app/navigators/HomeStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { createStackNavigator } from '@react-navigation/stack'
import React from 'react'
import { useTranslation } from 'react-i18next'

import HistoryMenu from '../components/History/HistoryMenu'
import SettingsMenu from '../components/buttons/SettingsMenu'
import { useStore } from '../contexts/store'
import { useTheme } from '../contexts/theme'
import Home from '../screens/Home'
import ListNotifications from '../screens/ListNotifications'
Expand All @@ -14,6 +16,7 @@ const HomeStack: React.FC = () => {
const Stack = createStackNavigator<HomeStackParams>()
const theme = useTheme()
const { t } = useTranslation()
const [store] = useStore()
const defaultStackOptions = createDefaultStackOptions(theme)

return (
Expand All @@ -23,7 +26,7 @@ const HomeStack: React.FC = () => {
component={Home}
options={() => ({
title: t('Screens.Home'),
headerRight: () => null,
headerRight: () => (store.preferences.useHistoryCapability ? <HistoryMenu /> : null),
headerLeft: () => <SettingsMenu />,
})}
/>
Expand Down
2 changes: 2 additions & 0 deletions app/navigators/RootStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { testIdWithKey } from '../utils/testable'
import ConnectStack from './ConnectStack'
import ContactStack from './ContactStack'
import DeliveryStack from './DeliveryStack'
import HistoryStack from './HistoryStack'
import NotificationStack from './NotificationStack'
import ProofRequestStack from './ProofRequestStack'
import SettingStack from './SettingStack'
Expand Down Expand Up @@ -290,6 +291,7 @@ const RootStack: React.FC = () => {
<Stack.Screen name={Stacks.NotificationStack} component={NotificationStack} />
<Stack.Screen name={Stacks.ConnectionStack} component={DeliveryStack} />
<Stack.Screen name={Stacks.ProofRequestsStack} component={ProofRequestStack} />
<Stack.Screen name={Stacks.HistoryStack} component={HistoryStack} />
</Stack.Navigator>
)
}
Expand Down
43 changes: 41 additions & 2 deletions app/screens/ContactDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,19 @@ import {
} from '@adeya/ssi'
import { useNavigation } from '@react-navigation/core'
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'
import React, { useCallback, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DeviceEventEmitter, View, Text, TouchableOpacity, StyleSheet } from 'react-native'
import { DeviceEventEmitter, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import Toast from 'react-native-toast-message'

import { saveHistory } from '../components/History/HistoryManager'
import { HistoryCardType, HistoryRecord } from '../components/History/types'
import Button, { ButtonType } from '../components/buttons/Button'
import CommonRemoveModal from '../components/modals/CommonRemoveModal'
import { ToastType } from '../components/toast/BaseToast'
import { EventTypes } from '../constants'
import { useStore } from '../contexts/store'
import { useTheme } from '../contexts/theme'
import { BifoldError } from '../types/error'
import { ContactStackParams, Screens, TabStacks } from '../types/navigators'
Expand All @@ -39,6 +42,7 @@ const ContactDetails: React.FC<ContactDetailsProps> = ({ route }) => {
const [isCredentialsOfferRemoveModalDisplayed, setIsCredentialsOfferRemoveModalDisplayed] = useState<boolean>(false)
const [isProofRequestRemoveModalDisplayed, setIsProofRequestRemoveModalDisplayed] = useState<boolean>(false)
const connection = useConnectionById(connectionId)
const [store] = useStore()
// FIXME: This should be exposed via a react hook that allows to filter credentials by connection id
const connectionCredentials = [
...useCredentialByState(CredentialState.CredentialReceived),
Expand Down Expand Up @@ -140,6 +144,41 @@ const ContactDetails: React.FC<ContactDetailsProps> = ({ route }) => {
const callCancelUnableToRemoveProofRequest = useCallback(() => handleProofRequestCancelUnableRemove(), [])

const contactLabel = useMemo(() => getConnectionName(connection) ?? '', [connection])

const logHistoryRecord = useCallback(async () => {
try {
if (!(agent && store.preferences.useHistoryCapability)) {
return
}
const type = HistoryCardType.Connection
if (!connection) {
return
}

try {
// Prepare the history record object
const recordData: HistoryRecord = {
type: type,
message: type,
createdAt: connection?.createdAt, // Assuming `data` has `createdAt` field
correspondenceId: connectionId,
connection: contactLabel,
}

// Save the history record asynchronously
await saveHistory(recordData, agent)
} catch (error) {
// error when save history
}
} catch (err: unknown) {
// error when agent and preferences not getting
}
}, [agent, store.preferences.useHistoryCapability, connection, connectionId])

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

const onDismissModalTouched = () => {
navigation.getParent()?.navigate(TabStacks.HomeStack, { screen: Screens.Home })
}
Expand Down
54 changes: 47 additions & 7 deletions app/screens/CredentialOffer.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
import {
useCredentialById,
AnonCredsCredentialOffer,
AnonCredsCredentialMetadataKey,
AnonCredsCredentialOffer,
AutoAcceptCredential,
CredentialPreviewAttribute,
JsonLdFormatDataCredentialDetail,
getFormattedCredentialData,
acceptCredentialOffer,
declineCredentialOffer,
getFormattedCredentialData,
sendCredentialProblemReport,
AutoAcceptCredential,
useConnections,
useCredentialById,
} from '@adeya/ssi'
import { BrandingOverlay } from '@hyperledger/aries-oca'
import { CredentialOverlay } from '@hyperledger/aries-oca/build/legacy'
import { StackScreenProps } from '@react-navigation/stack'
import React, { useEffect, useState } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { DeviceEventEmitter, StyleSheet, Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'

import { saveHistory } from '../components/History/HistoryManager'
import { HistoryCardType, HistoryRecord } from '../components/History/types'
import Button, { ButtonType } from '../components/buttons/Button'
import ConnectionImage from '../components/misc/ConnectionImage'
import CredentialCard from '../components/misc/CredentialCard'
import CommonRemoveModal from '../components/modals/CommonRemoveModal'
import Record from '../components/record/Record'
import W3CCredentialRecord from '../components/record/W3CCredentialRecord'
import { EventTypes } from '../constants'
import { CREDENTIAL_W3C, EventTypes } from '../constants'
import { useAnimatedComponents } from '../contexts/animated-components'
import { useConfiguration } from '../contexts/configuration'
import { useNetwork } from '../contexts/network'
import { useStore } from '../contexts/store'
import { useTheme } from '../contexts/theme'
import { BifoldError } from '../types/error'
import { TabStacks, NotificationStackParams, Screens } from '../types/navigators'
import { NotificationStackParams, Screens, TabStacks } from '../types/navigators'
import { W3CCredentialAttributeField } from '../types/record'
import { ModalUsage } from '../types/remove'
import { useAppAgent } from '../utils/agent'
import { parseCredDefFromId } from '../utils/cred-def'
import { buildFieldsFromJSONLDCredential, formatCredentialSubject, getCredentialIdentifiers } from '../utils/credential'
import { getCredentialConnectionLabel, getDefaultHolderDidDocument } from '../utils/helpers'
import { buildFieldsFromAnonCredsCredential } from '../utils/oca'
Expand Down Expand Up @@ -67,6 +71,7 @@ const CredentialOffer: React.FC<CredentialOfferProps> = ({ navigation, route })
const [tables, setTables] = useState<W3CCredentialAttributeField[]>([])
const { records } = useConnections()
const credentialConnectionLabel = getCredentialConnectionLabel(records, credential)
const [store] = useStore()

const styles = StyleSheet.create({
headerTextContainer: {
Expand Down Expand Up @@ -149,6 +154,39 @@ const CredentialOffer: React.FC<CredentialOfferProps> = ({ navigation, route })

const toggleDeclineModalVisible = () => setDeclineModalVisible(!declineModalVisible)

const logHistoryRecord = useCallback(
async (credentialType?: string, credentialName?: string) => {
try {
if (!(agent && store.preferences.useHistoryCapability)) {
return
}

const type = HistoryCardType.CardAccepted
if (!credential) {
return
}
const ids = getCredentialIdentifiers(credential)
const name =
credentialType === CREDENTIAL_W3C
? credentialName
: parseCredDefFromId(ids.credentialDefinitionId, ids.schemaId)

/** Save history record for card accepted */
const recordData: HistoryRecord = {
type: type,
message: type,
createdAt: credential?.createdAt,
correspondenceId: credentialId,
correspondenceName: name,
}
await saveHistory(recordData, agent)
} catch (err: unknown) {
// error when agent and preferences not getting
}
},
[agent, store.preferences.useHistoryCapability, credential, credentialId],
)

const handleAcceptTouched = async () => {
try {
if (!(agent && credential && assertConnectedNetwork())) {
Expand Down Expand Up @@ -184,8 +222,10 @@ const CredentialOffer: React.FC<CredentialOfferProps> = ({ navigation, route })
// we added auto accept credential to always accept the credential further flows
autoAcceptCredential: AutoAcceptCredential.Always,
})
await logHistoryRecord(CREDENTIAL_W3C, credentialFormatData?.offer?.jsonld?.credential?.type[1])
} else {
await acceptCredentialOffer(agent, { credentialRecordId: credential.id })
await logHistoryRecord()
}
} catch (err: unknown) {
setButtonsVisible(true)
Expand Down
28 changes: 28 additions & 0 deletions app/screens/Developer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const Developer: React.FC = () => {
!!store.preferences.useConnectionInviterCapability,
)

const [useHistoryCapability, setUseHistoryCapability] = useState(!!store.preferences.useHistoryCapability)
const [useDevVerifierTemplates, setDevVerifierTemplates] = useState(!!store.preferences.useDevVerifierTemplates)

const styles = StyleSheet.create({
Expand Down Expand Up @@ -68,6 +69,14 @@ const Developer: React.FC = () => {
setConnectionInviterCapability(previousState => !previousState)
}

const toggleHistoryCapabilitySwitch = () => {
dispatch({
type: DispatchAction.HISTORY_CAPABILITY,
payload: [!useHistoryCapability],
})
setUseHistoryCapability(previousState => !previousState)
}

const toggleDevVerifierTemplatesSwitch = () => {
// if we switch on dev templates we can assume the user also wants to enable the verifier capability
if (!useDevVerifierTemplates) {
Expand Down Expand Up @@ -127,6 +136,25 @@ const Developer: React.FC = () => {
/>
</Pressable>
</View>
<View style={styles.settingContainer}>
<View style={{ flex: 1 }}>
<Text style={styles.settingLabelText}>{t('History.UseHistoryCapability')}</Text>
</View>
<Pressable
style={styles.settingSwitchContainer}
accessibilityLabel={t('History.UseHistoryCapabilityToggle')}
accessibilityRole={'switch'}
testID={testIdWithKey('HistoryCapabilitySwitch')}>
<Switch
trackColor={{ false: ColorPallet.grayscale.lightGrey, true: ColorPallet.brand.primaryDisabled }}
thumbColor={useHistoryCapability ? ColorPallet.brand.primary : ColorPallet.grayscale.mediumGrey}
ios_backgroundColor={ColorPallet.grayscale.lightGrey}
onValueChange={toggleHistoryCapabilitySwitch}
testID={testIdWithKey('HistoryCapabilitySwitchElement')}
value={useHistoryCapability}
/>
</Pressable>
</View>
<View style={styles.settingContainer}>
<View style={{ flex: 1 }}>
<Text style={styles.settingLabelText}>{t('Verifier.UseDevVerifierTemplates')}</Text>
Expand Down
Loading

0 comments on commit 662cf96

Please sign in to comment.