Skip to content

Commit

Permalink
feat: refactored notifications (#2110)
Browse files Browse the repository at this point in the history
Signed-off-by: wadeking98 <[email protected]>
  • Loading branch information
wadeking98 authored Aug 6, 2024
1 parent d837acf commit e6d57f0
Show file tree
Hide file tree
Showing 12 changed files with 138 additions and 151 deletions.
4 changes: 3 additions & 1 deletion app/__tests__/screens/PersonCredential.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react'

import PersonCredential from '../../src/screens/PersonCredential'
import { initialState, reducer } from '../../src/store'
import { useNavigation } from '../../__mocks__/custom/@react-navigation/core'

const mockNavigation = jest.fn()
jest.mock('@react-navigation/native', () => ({
Expand Down Expand Up @@ -43,9 +44,10 @@ describe('Person Credential Screen', () => {
})

test('screen renders correctly', () => {
const navigation = useNavigation()
const tree = render(
<StoreProvider initialState={initialState} reducer={reducer}>
<PersonCredential />
<PersonCredential navigation={navigation as never} route={{} as never} />
</StoreProvider>
)

Expand Down
44 changes: 24 additions & 20 deletions app/container-imp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { DependencyContainer } from 'tsyringe'

import { autoDisableRemoteLoggingIntervalInMinutes } from './src/constants'
import { expirationOverrideInMinutes } from './src/helpers/utils'
import { useNotifications } from './src/hooks/notifications'
import Developer from './src/screens/Developer'
import PersonCredential from './src/screens/PersonCredential'
import PersonCredentialLoading from './src/screens/PersonCredentialLoading'
Expand Down Expand Up @@ -166,27 +167,30 @@ export class AppContainer implements Container {
})
this._container.registerInstance(TOKENS.UTIL_OCA_RESOLVER, resolver)

this._container.registerInstance(TOKENS.CUSTOM_NOTIFICATION, {
component: PersonCredential,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onCloseAction: (dispatch?: React.Dispatch<ReducerAction<any>>) => {
if (dispatch) {
dispatch({
type: BCDispatchAction.PERSON_CREDENTIAL_OFFER_DISMISSED,
payload: [{ personCredentialOfferDismissed: true }],
})
}
},
additionalStackItems: [
{
component: PersonCredentialLoading,
name: 'PersonCredentialLoading',
this._container.registerInstance(TOKENS.NOTIFICATIONS, {
useNotifications,
customNotificationConfig: {
component: PersonCredential,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onCloseAction: (dispatch?: React.Dispatch<ReducerAction<any>>) => {
if (dispatch) {
dispatch({
type: BCDispatchAction.PERSON_CREDENTIAL_OFFER_DISMISSED,
payload: [{ personCredentialOfferDismissed: true }],
})
}
},
],
pageTitle: 'PersonCredential.PageTitle',
title: 'PersonCredentialNotification.Title',
description: 'PersonCredentialNotification.Description',
buttonTitle: 'PersonCredentialNotification.ButtonTitle',
additionalStackItems: [
{
component: PersonCredentialLoading,
name: 'PersonCredentialLoading',
},
],
pageTitle: 'PersonCredential.PageTitle',
title: 'PersonCredentialNotification.Title',
description: 'PersonCredentialNotification.Description',
buttonTitle: 'PersonCredentialNotification.ButtonTitle',
},
})

this._container.registerInstance(TOKENS.UTIL_PROOF_TEMPLATE, useProofRequestTemplates)
Expand Down
4 changes: 2 additions & 2 deletions app/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ PODS:
- React-jsinspector (0.72.5)
- React-logger (0.72.5):
- glog
- "react-native-attestation (1.0.0-alpha.292+11561b58)":
- "react-native-attestation (1.0.0-alpha.295+b95c752a)":
- RCT-Folly (= 2021.07.22.00)
- React-Core
- react-native-config (1.5.0):
Expand Down Expand Up @@ -931,7 +931,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: ff70a72027dea5cc7d71cfcc6fad7f599f63987a
React-jsinspector: aef73cbd43b70675f572214d10fa438c89bf11ba
React-logger: 2e4aee3e11b3ec4fa6cfd8004610bbb3b8d6cca4
react-native-attestation: e4e6f8bfba8246606cafbd3e77c9b33fc6cb98b6
react-native-attestation: 190c81fbc066d101a947d6b9a73da6a455618a1c
react-native-config: 5330c8258265c1e5fdb8c009d2cabd6badd96727
react-native-encrypted-storage: db300a3f2f0aba1e818417c1c0a6be549038deb7
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
Expand Down
10 changes: 5 additions & 5 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@
"@formatjs/intl-relativetimeformat": "9.3.1",
"@hyperledger/anoncreds-react-native": "0.2.2",
"@hyperledger/aries-askar-react-native": "0.2.3",
"@hyperledger/aries-bifold-core": "1.0.0-alpha.292",
"@hyperledger/aries-bifold-remote-logs": "1.0.0-alpha.292",
"@hyperledger/aries-bifold-verifier": "1.0.0-alpha.292",
"@hyperledger/aries-oca": "1.0.0-alpha.292",
"@hyperledger/aries-react-native-attestation": "1.0.0-alpha.292",
"@hyperledger/aries-bifold-core": "1.0.0-alpha.295",
"@hyperledger/aries-bifold-remote-logs": "1.0.0-alpha.295",
"@hyperledger/aries-bifold-verifier": "1.0.0-alpha.295",
"@hyperledger/aries-oca": "1.0.0-alpha.295",
"@hyperledger/aries-react-native-attestation": "1.0.0-alpha.295",
"@hyperledger/indy-vdr-react-native": "0.2.2",
"@hyperledger/indy-vdr-shared": "0.2.2",
"@react-native-async-storage/async-storage": "1.15.11",
Expand Down
6 changes: 5 additions & 1 deletion app/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ export const autoDisableRemoteLoggingIntervalInMinutes = 60
export const surveyMonkeyUrl = 'https://www.surveymonkey.com/r/7BMHJL8'
export const surveyMonkeyExitUrl = 'https://www.surveymonkey.com/survey-thanks'
export const hitSlop = { top: 44, bottom: 44, left: 44, right: 44 }
export const AttestationRestrictions = {
interface AttestationRestrictionEnvironment {
credDefIDs: readonly string[]
invitationUrl: string
}
export const AttestationRestrictions: { [key: string]: AttestationRestrictionEnvironment } = {
Development: {
credDefIDs: ['NXp6XcGeCR2MviWuY51Dva:3:CL:33557:bcwallet', 'NXp6XcGeCR2MviWuY51Dva:3:CL:33557:bcwallet_dev_v2'],
invitationUrl:
Expand Down
1 change: 0 additions & 1 deletion app/src/helpers/BCIDHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ export const authenticateWithServiceCard = async (
forceCloseOnRedirection: false,
showInRecents: true,
})

// When `result.type` is "Cancel" that comes from the alert box or
// secure browser `result.url` will be undefined.
if (
Expand Down
143 changes: 61 additions & 82 deletions app/src/hooks/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,102 +21,81 @@ import { AttestationRestrictions } from '../constants'
import { showPersonCredentialSelector } from '../helpers/BCIDHelper'
import { isProofRequestingAttestation } from '../services/attestation'
import { BCState } from '../store'
interface CustomNotification {
type: 'CustomNotification'
createdAt: Date
id: string
}

interface Notifications {
total: number
notifications: Array<BasicMessageRecord | CredentialRecord | ProofExchangeRecord | CustomNotification>
}

export const useNotifications = (): Notifications => {
export const useNotifications = (): Array<BasicMessageRecord | CredentialRecord | ProofExchangeRecord> => {
const { agent } = useAgent()
const [store] = useStore<BCState>()
const offers = useCredentialByState(CredentialState.OfferReceived)
const proofsRequested = useProofByState(ProofState.RequestReceived)
const [nonAttestationProofs, setNonAttestationProofs] = useState<ProofExchangeRecord[]>([])
const [notifications, setNotifications] = useState([])
const { records: basicMessages } = useBasicMessages()
// get all unseen messages
const unseenMessages: BasicMessageRecord[] = basicMessages.filter((msg) => {
const meta = msg.metadata.get(BasicMessageMetadata.customMetadata) as basicMessageCustomMetadata
return !meta?.seen
})
// add one unseen message per contact to notifications
const contactsWithUnseenMessages: string[] = []
const messagesToShow: BasicMessageRecord[] = []
unseenMessages.forEach((msg) => {
if (!contactsWithUnseenMessages.includes(msg.connectionId)) {
contactsWithUnseenMessages.push(msg.connectionId)
messagesToShow.push(msg)
}
})
const proofsDone = useProofByState([ProofState.Done, ProofState.PresentationReceived]).filter(
(proof: ProofExchangeRecord) => {
if (proof.isVerified === undefined) return false

const metadata = proof.metadata.get(ProofMetadata.customMetadata) as ProofCustomMetadata
return !metadata?.details_seen
}
)
const credsReceived = useCredentialByState(CredentialState.CredentialReceived)
const credsDone = useCredentialByState(CredentialState.Done)
const proofsDone = useProofByState([ProofState.Done, ProofState.PresentationReceived])

useEffect(() => {
const getNonAttestationProofs = async () => {
const nonAttestationProofs = (
await Promise.all(
[...proofsRequested, ...proofsDone].map(async (proof: ProofExchangeRecord) => {
const isAttestation = await isProofRequestingAttestation(
proof,
agent as BifoldAgent,
AttestationRestrictions
)
return {
value: proof,
include: !isAttestation,
}
})
)
)
.filter((v) => v.include)
.map((data) => data.value)
// get all unseen messages
const unseenMessages: BasicMessageRecord[] = basicMessages.filter((msg) => {
const meta = msg.metadata.get(BasicMessageMetadata.customMetadata) as basicMessageCustomMetadata
return !meta?.seen
})

setNonAttestationProofs(nonAttestationProofs)
}
getNonAttestationProofs()
}, [proofsRequested.length, proofsDone.length])
// add one unseen message per contact to notifications
const contactsWithUnseenMessages: string[] = []
const messagesToShow: BasicMessageRecord[] = []
unseenMessages.forEach((msg) => {
if (!contactsWithUnseenMessages.includes(msg.connectionId)) {
contactsWithUnseenMessages.push(msg.connectionId)
messagesToShow.push(msg)
}
})

const revoked = useCredentialByState(CredentialState.Done).filter((cred: CredentialRecord) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const metadata = cred!.metadata.get(CredentialMetadata.customMetadata) as credentialCustomMetadata
if (cred?.revocationNotification && metadata?.revoked_seen == undefined) {
return cred
}
})
const revoked = credsDone.filter((cred: CredentialRecord) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const metadata = cred!.metadata.get(CredentialMetadata.customMetadata) as credentialCustomMetadata
if (cred?.revocationNotification && metadata?.revoked_seen == undefined) {
return cred
}
})

const credentials = [
...useCredentialByState(CredentialState.CredentialReceived),
...useCredentialByState(CredentialState.Done),
]
const credentialDefinitionIDs = credentials.map(
(c) => c.metadata.data[AnonCredsCredentialMetadataKey].credentialDefinitionId as string
)
const invitationDate = new Date()
const custom: CustomNotification[] =
showPersonCredentialSelector(credentialDefinitionIDs) &&
!store.dismissPersonCredentialOffer.personCredentialOfferDismissed
? [{ type: 'CustomNotification', createdAt: invitationDate, id: 'custom' }]
: []
const credentials = [...credsDone, ...credsReceived]
const credentialDefinitionIDs = credentials.map(
(c) => c.metadata.data[AnonCredsCredentialMetadataKey].credentialDefinitionId as string
)
const invitationDate = new Date()
const custom =
showPersonCredentialSelector(credentialDefinitionIDs) &&
!store.dismissPersonCredentialOffer.personCredentialOfferDismissed
? [{ type: 'CustomNotification', createdAt: invitationDate, id: 'custom' }]
: []

let notifications: (BasicMessageRecord | CredentialRecord | ProofExchangeRecord | CustomNotification)[] = [
...messagesToShow,
...offers,
...nonAttestationProofs,
...revoked,
].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
const notif = [...messagesToShow, ...offers, ...nonAttestationProofs, ...revoked].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)

notifications = [...custom, ...notifications]
const notificationsWithCustom = [...custom, ...notif]
setNotifications(notificationsWithCustom as never[])
}, [credsReceived, credsDone, basicMessages, nonAttestationProofs])

useEffect(() => {
const validProofsDone = proofsDone.filter((proof: ProofExchangeRecord) => {
if (proof.isVerified === undefined) return false

const metadata = proof.metadata.get(ProofMetadata.customMetadata) as ProofCustomMetadata
return !metadata?.details_seen
})
Promise.all(
[...proofsRequested, ...validProofsDone].map(async (proof: ProofExchangeRecord) => {
const isAttestation = await isProofRequestingAttestation(proof, agent as BifoldAgent, AttestationRestrictions)
return {
value: proof,
include: !isAttestation,
}
})
).then((val) => setNonAttestationProofs(val.filter((v) => v.include).map((data) => data.value)))
}, [proofsRequested, proofsDone])

return { total: notifications.length, notifications }
return notifications
}
2 changes: 0 additions & 2 deletions app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import HomeFooterView from './components/HomeFooterView'
import HomeHeaderView from './components/HomeHeaderView'
import { AttestationRestrictions } from './constants'
import { setup, activate, deactivate, status } from './helpers/PushNotificationsHelper'
import { useNotifications } from './hooks/notifications'
import en from './localization/en'
import fr from './localization/fr'
import ptBr from './localization/pt-br'
Expand Down Expand Up @@ -53,7 +52,6 @@ const configuration: ConfigurationContext = {
enableTours: true,
showPreface: true,
disableOnboardingSkip: true,
useCustomNotifications: useNotifications,
enablePushNotifications: {
status: status,
setup: setup,
Expand Down
14 changes: 7 additions & 7 deletions app/src/screens/PersonCredential.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAgent } from '@credo-ts/react-hooks'
import { useStore, useTheme, Button, ButtonType, testIdWithKey, Stacks, Link } from '@hyperledger/aries-bifold-core'
import { useNavigation } from '@react-navigation/native'
import { useStore, useTheme, Button, ButtonType, testIdWithKey, Link } from '@hyperledger/aries-bifold-core'
import { NotificationStackParams, Screens } from '@hyperledger/aries-bifold-core/lib/typescript/App/types/navigators'
import { StackScreenProps } from '@react-navigation/stack'
import React, { useState, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View, TouchableOpacity, Linking, Platform, ScrollView } from 'react-native'
Expand All @@ -18,13 +19,14 @@ const links = {
Help: 'https://www2.gov.bc.ca/gov/content/governments/government-id/person-credential#help',
} as const

const PersonCredential: React.FC = () => {
type PersonProps = StackScreenProps<NotificationStackParams, Screens.CustomNotification>

const PersonCredential: React.FC<PersonProps> = ({ navigation }) => {
const { agent } = useAgent()
const [store] = useStore<BCState>()
const [appInstalled, setAppInstalled] = useState<boolean>(false)
const { ColorPallet, TextTheme } = useTheme()
const { t } = useTranslation()
const navigation = useNavigation()

const styles = StyleSheet.create({
pageContainer: {
Expand Down Expand Up @@ -102,9 +104,7 @@ const PersonCredential: React.FC = () => {
return
}

navigation.getParent()?.navigate(Stacks.NotificationStack, {
screen: 'PersonCredentialLoading',
})
navigation.replace('PersonCredentialLoading' as never, {} as never)
}, [])

const getBCServicesCardApp = useCallback(() => {
Expand Down
1 change: 1 addition & 0 deletions app/src/screens/PersonCredentialLoading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ const PersonCredentialLoading: React.FC = () => {
// user reasons or otherwise.
if (!status) {
setDidCompleteAttestationProofRequest(false)
navigation.goBack()
}
}

Expand Down
10 changes: 5 additions & 5 deletions app/src/services/attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ type Restriction = {
}

const findCredDefIDs = (restrictions: [Restriction]): Array<string> => {
return restrictions.map((rstr) => rstr.cred_def_id).filter((credDefId) => credDefId !== undefined)
return restrictions.map((rstr) => rstr.cred_def_id).filter((credDefId) => credDefId !== undefined) as string[]
}

const invitationUrlFromRestrictions = async (
Expand All @@ -110,12 +110,12 @@ const invitationUrlFromRestrictions = async (
}

const pRestrictions = format.request?.[formatToUse]?.requested_attributes?.attestationInfo?.restrictions
const cred_def_ids = findCredDefIDs(pRestrictions as [any])
const cred_def_ids = findCredDefIDs(pRestrictions as [Restriction])

for (const env in restrictions) {
for (const credDefId of cred_def_ids) {
if ((restrictions as Record<string, any>)[env].credDefIDs.includes(credDefId)) {
return (restrictions as Record<string, any>)[env].invitationUrl
if (restrictions[env].credDefIDs.includes(credDefId)) {
return restrictions[env].invitationUrl
}
}
}
Expand All @@ -135,7 +135,7 @@ export const allCredDefIds = (restrictions: AttestationRestrictionsType): string
const allCredDefIds: string[] = []

for (const env in restrictions) {
allCredDefIds.push(...(restrictions as Record<string, any>)[env].credDefIDs)
allCredDefIds.push(...restrictions[env].credDefIDs)
}

return allCredDefIds
Expand Down
Loading

0 comments on commit e6d57f0

Please sign in to comment.