Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(react-hooks): extend credential providers with format data #146

Closed
Closed
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
62 changes: 42 additions & 20 deletions packages/react-hooks/src/CredentialProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import type { RecordsState } from './recordUtils'
import type { Agent, CredentialState } from '@aries-framework/core'
import type { CombinedRecordsState, RecordsState } from './recordUtils'
import type { Agent, CredentialState, GetFormatDataReturn, IndyCredentialFormat } from '@aries-framework/core'
import type { PropsWithChildren } from 'react'

import { CredentialExchangeRecord } from '@aries-framework/core'
import { useState, createContext, useContext, useEffect, useMemo } from 'react'
import * as React from 'react'

import {
addCombinedRecord,
removeCombinedRecord,
updateCombinedRecord,
recordsRemovedByType,
recordsUpdatedByType,
recordsAddedByType,
removeRecord,
updateRecord,
addRecord,
} from './recordUtils'

const CredentialContext = createContext<RecordsState<CredentialExchangeRecord> | undefined>(undefined)
const CredentialContext = createContext<CombinedRecordsState<CredentialExchangeRecord> | undefined>(undefined)

export const useCredentials = () => {
export const useCombinedCredentials = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe useCredentialsWithFormatData()? Or do you think that is too long?

combined doesn't really give any context on what is combined

const credentialContext = useContext(CredentialContext)
if (!credentialContext) {
throw new Error('useCredentials must be used within a CredentialContextProvider')
}
return credentialContext
}

export const useCredentials = (): RecordsState<CredentialExchangeRecord> => {
const { records: combinedRecords, loading } = useCombinedCredentials()
return { records: combinedRecords.map(({ record }) => record), loading }
}

export const useCredentialById = (id: string): CredentialExchangeRecord | undefined => {
const { records: credentials } = useCredentials()
return credentials.find((c: CredentialExchangeRecord) => c.id === id)
Expand All @@ -39,20 +44,34 @@ export const useCredentialByState = (state: CredentialState): CredentialExchange
return filteredCredentials
}

export const useFormatDataForCredentialById = (id: string): GetFormatDataReturn<[IndyCredentialFormat]> | undefined => {
const { records: combinedRecords } = useCombinedCredentials()
return combinedRecords.find(({ record: c }) => c.id === id)?.formatData
}

interface Props {
agent: Agent | undefined
}

const CredentialProvider: React.FC<PropsWithChildren<Props>> = ({ agent, children }) => {
const [state, setState] = useState<RecordsState<CredentialExchangeRecord>>({
const [state, setState] = useState<CombinedRecordsState<CredentialExchangeRecord>>({
records: [],
loading: true,
})

const combineRecord = async (record: CredentialExchangeRecord) => {
let formatData: GetFormatDataReturn<[IndyCredentialFormat]> = {}
if (agent) {
formatData = await agent.credentials.getFormatData(record.id)
}
return { record, formatData }
}

const setInitialState = async () => {
if (agent) {
const records = await agent.credentials.getAll()
setState({ records, loading: false })
const combinedRecords = await Promise.all(records.map(combineRecord))
setState({ records: combinedRecords, loading: false })
}
}

Expand All @@ -62,17 +81,20 @@ const CredentialProvider: React.FC<PropsWithChildren<Props>> = ({ agent, childre

useEffect(() => {
if (!state.loading) {
const credentialAdded$ = recordsAddedByType(agent, CredentialExchangeRecord).subscribe((record) =>
setState(addRecord(record, state))
)

const credentialUpdated$ = recordsUpdatedByType(agent, CredentialExchangeRecord).subscribe((record) =>
setState(updateRecord(record, state))
)

const credentialRemoved$ = recordsRemovedByType(agent, CredentialExchangeRecord).subscribe((record) =>
setState(removeRecord(record, state))
)
const credentialAdded$ = recordsAddedByType(agent, CredentialExchangeRecord).subscribe(async (record) => {
const combinedRecord = await combineRecord(record)
return setState(addCombinedRecord(combinedRecord, state))
})

const credentialUpdated$ = recordsUpdatedByType(agent, CredentialExchangeRecord).subscribe(async (record) => {
const combinedRecord = await combineRecord(record)
setState(updateCombinedRecord(combinedRecord, state))
})

const credentialRemoved$ = recordsRemovedByType(agent, CredentialExchangeRecord).subscribe(async (record) => {
const combinedRecord = await combineRecord(record)
setState(removeCombinedRecord(combinedRecord, state))
})

return () => {
credentialAdded$?.unsubscribe()
Expand Down
55 changes: 54 additions & 1 deletion packages/react-hooks/src/recordUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type {
RecordUpdatedEvent,
Agent,
BaseEvent,
GetFormatDataReturn,
IndyCredentialFormat,
} from '@aries-framework/core'
import type { Constructor } from '@aries-framework/core/build/utils/mixins'

Expand All @@ -14,9 +16,20 @@ import { map, filter, pipe } from 'rxjs'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type BaseRecordAny = BaseRecord<any, any, any>
type RecordClass<R extends BaseRecordAny> = Constructor<R> & { type: string }

interface CombinedRecord<R extends BaseRecord> {
record: R
formatData: GetFormatDataReturn<[IndyCredentialFormat]>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to only specifically do IndyCredentialFormat I think we should name it accordingly.

I also implemented this locally for some projects and just made the formatData a separate hook.

const formattedCredential = useCredentialFormatDataById(credentialId)

or

const formattedCredential = useCredentialFormatData()

Which could then optionally take a generic for the format data, i.e. IndyCredentialFormat. I could also create a PR with this code (and for proofs.)

}

export interface RecordsState<R extends BaseRecordAny> {
loading: boolean
records: R[]
records: Array<R>
}

export interface CombinedRecordsState<R extends BaseRecordAny> {
loading: boolean
records: Array<CombinedRecord<R>>
}

export const addRecord = <R extends BaseRecordAny>(record: R, state: RecordsState<R>): RecordsState<R> => {
Expand Down Expand Up @@ -48,6 +61,46 @@ export const removeRecord = <R extends BaseRecordAny>(record: R, state: RecordsS
}
}

export const addCombinedRecord = <R extends BaseRecordAny>(
combinedRecord: CombinedRecord<R>,
state: CombinedRecordsState<R>
): CombinedRecordsState<R> => {
const newRecordsState = [...state.records]
newRecordsState.unshift(combinedRecord)
return {
loading: state.loading,
records: newRecordsState,
}
}

export const updateCombinedRecord = <R extends BaseRecordAny>(
combinedRecord: CombinedRecord<R>,
state: CombinedRecordsState<R>
): CombinedRecordsState<R> => {
const { record } = combinedRecord
const newRecordsState = [...state.records]
const index = newRecordsState.findIndex(({ record: r }) => r.id === record.id)
if (index > -1) {
newRecordsState[index] = combinedRecord
}
return {
loading: state.loading,
records: newRecordsState,
}
}

export const removeCombinedRecord = <R extends BaseRecordAny>(
combinedRecord: CombinedRecord<R>,
state: CombinedRecordsState<R>
): CombinedRecordsState<R> => {
const { record } = combinedRecord
const newRecordsState = state.records.filter(({ record: r }) => r.id !== record.id)
return {
loading: state.loading,
records: newRecordsState,
}
}

const filterByType = <R extends BaseRecordAny>(recordClass: RecordClass<R>) => {
return pipe(
map((event: BaseEvent) => (event.payload as Record<string, R>).record),
Expand Down