Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions app/components/ApprovalControls/ApprovalControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ enum StatusIcon {
type ApprovalControlsProps = {
pendingCredential: PendingCredential
profileRecordId: ObjectID
profileName?: string
}

type ApprovalButtonProps = {
Expand All @@ -53,14 +54,21 @@ const colorFor = (status: ApprovalStatus, theme: ThemeType): Color =>
[ApprovalStatus.Accepted]: theme.color.success
})[status]

const defaultMessageFor = (status: ApprovalStatus): ApprovalMessage =>
({
const defaultMessageFor = (
status: ApprovalStatus,
profileName?: string
): ApprovalMessage => {
if (status === ApprovalStatus.PendingDuplicate && profileName) {
return `This credential already exists in "${profileName}" and cannot be added again.` as ApprovalMessage
}
return {
[ApprovalStatus.Pending]: ApprovalMessage.Pending,
[ApprovalStatus.PendingDuplicate]: ApprovalMessage.Duplicate,
[ApprovalStatus.Accepted]: ApprovalMessage.Accepted,
[ApprovalStatus.Rejected]: ApprovalMessage.Rejected,
[ApprovalStatus.Errored]: ApprovalMessage.Errored
})[status]
}[status]
}

function ApprovalButton({
title,
Expand All @@ -84,12 +92,13 @@ function ApprovalButton({

export default function ApprovalControls({
pendingCredential,
profileRecordId
profileRecordId,
profileName
}: ApprovalControlsProps): React.ReactElement {
const { styles, theme } = useDynamicStyles(dynamicStyleSheet)
const dispatch = useAppDispatch()
const { status, messageOverride } = pendingCredential
const message = messageOverride || defaultMessageFor(status)
const message = messageOverride || defaultMessageFor(status, profileName)
const [statusRef, focusStatus] = useAccessibilityFocus<View>()

function setApprovalStatus(status: ApprovalStatus) {
Expand Down Expand Up @@ -143,10 +152,10 @@ export default function ApprovalControls({
case ApprovalStatus.PendingDuplicate:
return (
<>
<Text style={styles.statusTextOutside}>{message}</Text>
<View style={styles.approvalContainer}>
<ApprovalButton title="Close" onPress={rejectAndExit} primary />
</View>
<Text style={styles.statusTextOutside}>{message}</Text>
</>
)
default:
Expand Down
7 changes: 1 addition & 6 deletions app/mock/credential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,7 @@ export const rawVcRecords: CredentialRecordRaw[] = [
}
]

const credentials = [
mockCredential,
mockCredential2
// studentCard,
// anotherCred,
]
const credentials = [mockCredential, mockCredential2]

export default mockCredential
export { credentials }
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type AcceptCredentialsNavigationParamList = {
ApproveCredentialsScreen: {
credentialRequestParams?: CredentialRequestParams
rawProfileRecord: ProfileRecordRaw
canGoBack?: boolean
}
ApproveCredentialScreen: {
pendingCredentialId: string
Expand Down
3 changes: 3 additions & 0 deletions app/navigation/RootNavigation/RootNavigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export default function RootNavigation(): React.ReactElement {
<Stack.Screen
name="AcceptCredentialsNavigation"
component={AcceptCredentialsNavigation}
options={{
gestureEnabled: false
}}
/>
<Stack.Screen
name="ExchangeCredentialsNavigation"
Expand Down
83 changes: 76 additions & 7 deletions app/screens/ApproveCredentialsScreen/ApproveCredentialsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React, { useMemo, useState } from 'react'
import React, { useMemo, useState, useEffect } from 'react'
import { useSelector } from 'react-redux'
import { FlatList, View, Text } from 'react-native'
import { Button } from 'react-native-elements'
import { useFocusEffect } from '@react-navigation/native'

import { navigationRef } from '../../navigation/navigationRef'
import {
acceptPendingCredentials,
ApprovalStatus,
clearFoyer,
selectPendingCredentials
selectPendingCredentials,
setCredentialApproval
} from '../../store/slices/credentialFoyer'
import { CredentialRecord } from '../../model/credential'
import { credentialContentHash } from '../../lib/credentialHash'
import CredentialItem from '../../components/CredentialItem/CredentialItem'
import NavHeader from '../../components/NavHeader/NavHeader'
import CredentialRequestHandler from '../../components/CredentialRequestHandler/CredentialRequestHandler'
Expand All @@ -30,7 +34,7 @@ export default function ApproveCredentialsScreen({
const { mixins, styles } = useDynamicStyles(dynamicStyleSheet)

const dispatch = useAppDispatch()
const { rawProfileRecord, credentialRequestParams } = route.params
const { rawProfileRecord, credentialRequestParams, canGoBack } = route.params
const profileRecordId = rawProfileRecord._id

const displayedCredentials = useSelector(selectPendingCredentials)
Expand All @@ -45,7 +49,64 @@ export default function ApproveCredentialsScreen({
)

const [modalIsOpen, setModalIsOpen] = useState(false)
const showAcceptAllButton = pendingCredentials.length > 1
const acceptableCredentials = useMemo(
() =>
pendingCredentials.filter(
({ status }) => status === ApprovalStatus.Pending
),
[pendingCredentials]
)
const showAcceptAllButton = acceptableCredentials.length > 0
const acceptButtonText =
acceptableCredentials.length === 1 ? 'Accept' : 'Accept All'

const handleGoBack = React.useCallback(async () => {
await dispatch(clearFoyer())
navigationRef.navigate('HomeNavigation', {
screen: 'SettingsNavigation',
params: { screen: 'DeveloperScreen' }
})
}, [dispatch])

useFocusEffect(
React.useCallback(() => {
const recheckDuplicates = async () => {
const existingCredentialRecords =
await CredentialRecord.getAllCredentialRecords()
const existingHashesInProfile = existingCredentialRecords
.filter(({ profileRecordId: pid }) => pid.equals(profileRecordId))
.map(({ credential }) => credentialContentHash(credential))

displayedCredentials.forEach((pendingCredential) => {
const isDuplicate = existingHashesInProfile.includes(
credentialContentHash(pendingCredential.credential)
)
if (
isDuplicate &&
pendingCredential.status === ApprovalStatus.Pending
) {
dispatch(
setCredentialApproval({
...pendingCredential,
status: ApprovalStatus.PendingDuplicate
})
)
} else if (
!isDuplicate &&
pendingCredential.status === ApprovalStatus.PendingDuplicate
) {
dispatch(
setCredentialApproval({
...pendingCredential,
status: ApprovalStatus.Pending
})
)
}
})
}
recheckDuplicates()
}, [displayedCredentials, profileRecordId, dispatch])
)

async function goToHome() {
await dispatch(clearFoyer())
Expand All @@ -62,7 +123,10 @@ export default function ApproveCredentialsScreen({
async function acceptAllCredentials() {
try {
await dispatch(
acceptPendingCredentials({ pendingCredentials, profileRecordId })
acceptPendingCredentials({
pendingCredentials: acceptableCredentials,
profileRecordId
})
)
goToHome()
} catch (err) {
Expand Down Expand Up @@ -101,6 +165,7 @@ export default function ApproveCredentialsScreen({
<ApprovalControls
pendingCredential={pendingCredential}
profileRecordId={profileRecordId}
profileName={rawProfileRecord.profileName}
/>
}
chevron
Expand All @@ -110,7 +175,11 @@ export default function ApproveCredentialsScreen({

return (
<>
<NavHeader title="Available Credentials" rightComponent={<Done />} />
<NavHeader
title="Available Credentials"
goBack={canGoBack ? handleGoBack : undefined}
rightComponent={<Done />}
/>
<CredentialRequestHandler
credentialRequestParams={credentialRequestParams}
rawProfileRecord={rawProfileRecord}
Expand Down Expand Up @@ -142,7 +211,7 @@ export default function ApproveCredentialsScreen({
styles.acceptAllButton
]}
titleStyle={[mixins.buttonTitle, styles.acceptAllButtonTitle]}
title="Accept All"
title={acceptButtonText}
onPress={acceptAllCredentials}
/>
</SafeAreaView>
Expand Down
40 changes: 27 additions & 13 deletions app/screens/DeveloperScreen/DeveloperScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
import { NavHeader } from '../../components'
import dynamicStyleSheet from './DeveloperScreen.styles'
import { DeveloperScreenProps } from './DeveloperScreen.d'
import { stageCredentials } from '../../store/slices/credentialFoyer'
import { stageCredentialsForProfile } from '../../store/slices/credentialFoyer'
import { credentials } from '../../mock/credential'
import { navigationRef } from '../../navigation/navigationRef'
import { Cache, CacheKey } from '../../lib/cache'
Expand Down Expand Up @@ -48,26 +48,40 @@ export default function DeveloperScreen({
titleStyle: mixins.buttonIconTitle
}

async function goToApproveCredentials() {
if (navigationRef.isReady()) {
async function addMockCredentials() {
try {
const rawProfileRecord = await NavigationUtil.selectProfile()
await dispatch(
stageCredentialsForProfile({
credentials,
profileRecordId: rawProfileRecord._id
})
).unwrap()
navigationRef.navigate('AcceptCredentialsNavigation', {
screen: 'ApproveCredentialsScreen',
params: {
rawProfileRecord
}
params: { rawProfileRecord, canGoBack: true }
})
} catch (error) {
console.error('Error adding mock credentials:', error)
}
}

async function addMockCredentials() {
await dispatch(stageCredentials(credentials))
goToApproveCredentials()
}

async function addRevokedCredential() {
await dispatch(stageCredentials([revokedCredential]))
goToApproveCredentials()
try {
const rawProfileRecord = await NavigationUtil.selectProfile()
await dispatch(
stageCredentialsForProfile({
credentials: [revokedCredential],
profileRecordId: rawProfileRecord._id
})
).unwrap()
navigationRef.navigate('AcceptCredentialsNavigation', {
screen: 'ApproveCredentialsScreen',
params: { rawProfileRecord, canGoBack: true }
})
} catch (error) {
console.error('Error adding revoked credential:', error)
}
}

function receiveCredentialThroughDeepLink() {
Expand Down
23 changes: 18 additions & 5 deletions app/screens/ProfileSelectionScreen/ProfileSelectionScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useRef } from 'react'
import React, { useEffect, useMemo, useRef, useCallback } from 'react'
import { FlatList, Text, View } from 'react-native'
import { Button, ListItem } from 'react-native-elements'

Expand All @@ -11,6 +11,7 @@ import { NavHeader } from '../../components'
import { useSelector } from 'react-redux'
import { selectRawProfileRecords } from '../../store/slices/profile'
import { useDynamicStyles, useSelectorFactory } from '../../hooks'
import { ProfileRecordRaw } from '../../model'

import { makeSelectProfileForPendingCredentials } from '../../store/selectorFactories/makeSelectProfileForPendingCredentials'

Expand All @@ -35,15 +36,27 @@ export default function ProfileSelectionScreen({
[rawProfileRecords]
)

const hasAutoSelected = useRef(false)

const handleSelectProfile = useCallback(
(profile: ProfileRecordRaw) => {
hasAutoSelected.current = true
onSelectProfile(profile)
},
[onSelectProfile]
)

useEffect(() => {
console.log('Profile records:', rawProfileRecords)

if (hasAutoSelected.current) return

if (associatedProfile) {
onSelectProfile(associatedProfile)
handleSelectProfile(associatedProfile)
} else if (rawProfileRecords.length === 1) {
onSelectProfile(rawProfileRecords[0])
handleSelectProfile(rawProfileRecords[0])
}
}, [associatedProfile, rawProfileRecords, onSelectProfile])
}, [associatedProfile, rawProfileRecords, handleSelectProfile])

const ListHeader = (
<View style={styles.listHeader}>
Expand All @@ -61,7 +74,7 @@ export default function ProfileSelectionScreen({
renderItem={({ item }) => (
<ProfileButton
rawProfileRecord={item}
onPress={() => onSelectProfile(item)}
onPress={() => handleSelectProfile(item)}
/>
)}
/>
Expand Down
6 changes: 5 additions & 1 deletion app/store/slices/credentialFoyer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { canonicalize as jcsCanonicalize } from 'json-canonicalize'
import { CredentialRecord } from '../../model/credential'

import { RootState } from '..'
import { addCredential } from './credential'
import { addCredential, deleteCredential } from './credential'
import { ObjectID } from 'bson'
import { IVerifiableCredential } from '@digitalcredentials/ssi'
import { credentialContentHash } from '../../lib/credentialHash'
Expand Down Expand Up @@ -239,6 +239,10 @@ const credentialFoyer = createSlice({
builder.addCase(acceptPendingCredentials.rejected, (_, action) => {
throw action.error
})

builder.addCase(deleteCredential.fulfilled, (state) => {
return state
})
}
})

Expand Down
Loading