Skip to content

Commit

Permalink
feat: trust ui and usage (#189)
Browse files Browse the repository at this point in the history
Signed-off-by: Jan <[email protected]>
  • Loading branch information
janrtvld authored Oct 30, 2024
1 parent 27a7432 commit 108ebb1
Show file tree
Hide file tree
Showing 55 changed files with 1,244 additions and 566 deletions.
Binary file modified apps/easypid/assets/german-issuer-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/easypid/src/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default function AppLayout() {
<Stack.Screen name="activity/[id]" options={headerNormalOptions} />
<Stack.Screen name="pinConfirmation" options={headerNormalOptions} />
<Stack.Screen name="pinLocked" options={headerNormalOptions} />
<Stack.Screen name="issuer" options={headerNormalOptions} />
</Stack>
</DeeplinkHandler>
</WalletJsonStoreProvider>
Expand Down
5 changes: 4 additions & 1 deletion apps/easypid/src/app/(app)/activity/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { FunkeActivityScreen } from '@easypid/features/activity/FunkeActivityScreen'
import { useLocalSearchParams } from 'expo-router'

export default function Screen() {
return <FunkeActivityScreen />
const { host } = useLocalSearchParams()

return <FunkeActivityScreen host={host as string} />
}
8 changes: 8 additions & 0 deletions apps/easypid/src/app/(app)/issuer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FunkeIssuerDetailScreen } from '@easypid/features/wallet/FunkeIssuerDetailScreen'
import { useLocalSearchParams } from 'expo-router'

export default function Screen() {
const { host } = useLocalSearchParams()

return <FunkeIssuerDetailScreen host={host as string} />
}
133 changes: 79 additions & 54 deletions apps/easypid/src/features/activity/FunkeActivityDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { FlexPage, Heading, HeroIcons, Paragraph, ScrollView, Stack, YStack } from '@package/ui'
import { Circle, FlexPage, Heading, Paragraph, ScrollView, Stack, YStack } from '@package/ui'
import React from 'react'
import { createParam } from 'solito'

import { TextBackButton, activityTitleMap } from '@package/app'
import { useCredentialsWithCustomDisplay } from '@easypid/hooks/useCredentialsWithCustomDisplay'
import { CardWithAttributes, TextBackButton, activityInteractions } from '@package/app'
import { useScrollViewPosition } from '@package/app/src/hooks'
import { useCredentialsForDisplay } from 'packages/agent/src'
import { formatRelativeDate } from 'packages/utils/src'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useRouter } from 'solito/router'
import { CardWithAttributes } from '../share/components/RequestedAttributesSection'
import { RequestPurposeSection } from '../share/components/RequestPurposeSection'
import { useActivities } from './activityRecord'
import { FailedReasonContainer } from './components/FailedReasonContainer'

const { useParams } = createParam<{ id: string }>()

Expand All @@ -18,7 +20,7 @@ export function FunkeActivityDetailScreen() {
const { bottom } = useSafeAreaInsets()

const { activities } = useActivities()
const { credentials } = useCredentialsForDisplay()
const { credentials } = useCredentialsWithCustomDisplay()
const activity = activities.find((activity) => activity.id === params.id)

if (!activity || activity.type === 'received') {
Expand All @@ -27,62 +29,85 @@ export function FunkeActivityDetailScreen() {
return
}

const Icon = activityInteractions[activity.type][activity.status]
const Title = activityInteractions[activity.type][activity.status].text

const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition()

return (
<FlexPage p={0} gap={0}>
<YStack bbw="$0.5" p="$4" borderColor={isScrolledByOffset ? '$grey-200' : '$background'} />
<ScrollView onScroll={handleScroll} scrollEventThrottle={scrollEventThrottle}>
<YStack jc="center" ai="center" p="$4">
<HeroIcons.ShieldCheckFilled strokeWidth={2} color="$positive-500" size={56} />
</YStack>
<YStack gap="$4" px="$4" marginBottom={bottom}>
<Stack gap="$2" ai="center">
<Heading textAlign="center" variant="h1">
{activityTitleMap[activity.type]}
</Heading>
<Paragraph textAlign="center" color="$grey-700">
You have shared this data with {activity.entityName ?? activity.entityHost} on{' '}
{new Date(activity.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
.
</Paragraph>
</Stack>
<Stack py="$4" gap="$4">
{activity.credentials && activity.credentials.length > 0 ? (
activity.credentials?.map((activityCredential) => {
const credential = credentials.find((credential) => credential.id.includes(activityCredential.id))
if (credential)
return (
<CardWithAttributes
key={credential.id}
id={credential.id}
name={credential.display.name}
backgroundColor={credential.display.backgroundColor}
backgroundImage={credential.display.backgroundImage}
disclosedAttributes={activityCredential.disclosedAttributes ?? []}
disclosedPayload={activityCredential.disclosedPayload ?? {}}
/>
)
return (
<CardWithAttributes
key={activityCredential.id}
id={activityCredential.id}
name="Deleted credential"
disclosedAttributes={activityCredential.disclosedAttributes ?? []}
disclosedPayload={activityCredential.disclosedPayload ?? {}}
/>
)
})
) : (
<Paragraph variant="annotation" ta="center">
Disclosed information could not be shown.
</Paragraph>
)}
<YStack gap="$4" marginBottom={bottom}>
<Stack h="$8" jc="center" ai="center" pos="relative">
<Circle pos="absolute" size={72} bg={Icon.color} opacity={0.1} />
<Circle pos="absolute" size={58} bg={Icon.color} opacity={0.2} />
<Circle size="$4" bg={Icon.color}>
<Icon.icon strokeWidth={2} color="$white" />
</Circle>
</Stack>
<YStack gap="$4" px="$4">
<Stack gap="$2" ai="center">
<Heading textAlign="center" variant="h1">
{Title}
</Heading>
<Paragraph textAlign="center">{formatRelativeDate(new Date(activity.date), undefined, true)}</Paragraph>
</Stack>
<Stack h={1} my="$2" bg="$grey-100" />
<Stack gap="$6">
<RequestPurposeSection
purpose={
activity.request.purpose ??
'No information was provided on the purpose of the data request. Be cautious'
}
logo={activity.entity.logo}
/>
<Stack gap="$3">
<Stack gap="$2">
<Heading variant="sub2">
{activity.status === 'success' ? 'Shared attributes' : 'Requested information'}
</Heading>
<Paragraph>
{activity.status === 'success' ? 'Credentials were shared' : 'No credentials were shared.'}
</Paragraph>
</Stack>
{activity.request.credentials && activity.request.credentials.length > 0 ? (
activity.request.credentials.map((activityCredential) => {
const credential = credentials.find((credential) => credential.id.includes(activityCredential.id))
if (credential)
return (
<CardWithAttributes
key={credential.id}
id={credential.id}
name={credential.display.name}
issuerImage={credential.display.issuer.logo}
textColor={credential.display.textColor}
backgroundColor={credential.display.backgroundColor}
backgroundImage={credential.display.backgroundImage}
disclosedAttributes={activityCredential.disclosedAttributes ?? []}
disclosedPayload={activityCredential.disclosedPayload ?? {}}
disableNavigation={activity.status !== 'success'}
/>
)
return (
<CardWithAttributes
key={activityCredential.id}
id={activityCredential.id}
name="Deleted credential"
textColor="$grey-100"
backgroundColor="$primary-500"
disclosedAttributes={activityCredential.disclosedAttributes ?? []}
disclosedPayload={activityCredential.disclosedPayload ?? {}}
disableNavigation={true}
/>
)
})
) : (
<FailedReasonContainer reason={activity.request.failureReason ?? 'unknown'} />
)}
</Stack>
</Stack>
</YStack>
</YStack>
</ScrollView>
<YStack btw="$0.5" borderColor="$grey-200" pt="$4" mx="$-4" px="$4" bg="$background">
Expand Down
17 changes: 9 additions & 8 deletions apps/easypid/src/features/activity/FunkeActivityScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import React, { useMemo } from 'react'
import { FadeInDown } from 'react-native-reanimated'
import { useActivities } from './activityRecord'

export function FunkeActivityScreen() {
const { activities, isLoading: isLoadingActivities } = useActivities()
export function FunkeActivityScreen({ host }: { host?: string }) {
const { activities, isLoading: isLoadingActivities } = useActivities({ filters: { host } })

const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition()

Expand All @@ -29,11 +29,9 @@ export function FunkeActivityScreen() {
return (
<FlexPage gap="$0" paddingHorizontal="$0">
<YStack w="100%" top={0} borderBottomWidth="$0.5" borderColor={isScrolledByOffset ? '$grey-200' : '$background'}>
<YStack gap="$4" p="$4">
<YStack gap="$2" p="$4">
<Stack h="$1" />
<Heading variant="h1" fontWeight="$bold">
Activity
</Heading>
<Heading variant="h1">Activity</Heading>
</YStack>
</YStack>
{activities.length === 0 ? (
Expand Down Expand Up @@ -68,17 +66,20 @@ export function FunkeActivityScreen() {
return (
<React.Fragment key={key}>
<Stack bbw={1} btw={1} borderColor="$grey-200" px="$4" py="$3" mx={-18}>
<Heading variant="h3" fontWeight="$semiBold">
<Heading variant="sub2">
{date.toLocaleString('default', { month: 'long', year: 'numeric' })}
</Heading>
</Stack>
{groupActivities.map((activity) => (
<ActivityRowItem
key={activity.id}
id={activity.id}
subtitle={activity.entityName ?? activity.entityHost}
logo={activity.entity.logo}
backgroundColor={activity.entity.backgroundColor}
subtitle={activity.entity.name ?? activity.entity.host ?? 'Unknown party'}
date={new Date(activity.date)}
type={activity.type}
status={activity.status}
// FIXME: Handle multiple credentials received in one request
credentialId={activity.type === 'received' ? activity.credentialIds?.[0] : undefined}
/>
Expand Down
112 changes: 100 additions & 12 deletions apps/easypid/src/features/activity/activityRecord.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import { type EasyPIDAppAgent, getWalletJsonStore, useWalletJsonRecord } from '@package/agent'
import { utils } from '@credo-ts/core'
import { type DisplayImage, type EasyPIDAppAgent, getWalletJsonStore, useWalletJsonRecord } from '@package/agent'
import { getHostNameFromUrl } from 'packages/utils/src'
import { useMemo } from 'react'

export type ActivityType = 'shared' | 'received'
export type ActivityStatus = 'success' | 'failed' | 'stopped'
export type SharingFailureReason = 'missing_credentials' | 'unknown'

interface BaseActivity {
id: string
type: ActivityType
status: ActivityStatus
date: string
entityHost: string
entityName?: string
entity: {
did: string
host?: string
name?: string
logo?: DisplayImage
backgroundColor?: string
}
}

interface PresentationActivity extends BaseActivity {
type: 'shared'
credentials?: Array<{
id: string
disclosedAttributes: string[]
disclosedPayload: Record<string, unknown>
}>
request: {
credentials: Array<{
id: string
disclosedAttributes: string[]
disclosedPayload: Record<string, unknown>
}>
name?: string
purpose?: string
failureReason?: SharingFailureReason
}
}

interface IssuanceActivity extends BaseActivity {
Expand All @@ -31,7 +46,7 @@ interface ActivityRecord {
activities: Activity[]
}

const _activityStorage = getWalletJsonStore<ActivityRecord>('EASYPID_WALLET_ACTIVITY_RECORD')
const _activityStorage = getWalletJsonStore<ActivityRecord>('EASYPID_ACTIVITY_RECORD')
export const activityStorage = {
recordId: _activityStorage.recordId,
addActivity: async (agent: EasyPIDAppAgent, activity: Activity) => {
Expand All @@ -48,17 +63,90 @@ export const activityStorage = {
},
}

export const useActivities = () => {
export const useActivities = ({ filters }: { filters?: { host?: string } } = {}) => {
const { record, isLoading } = useWalletJsonRecord<ActivityRecord>(activityStorage.recordId)

const activities = useMemo(() => {
if (!record?.activities) return []

return [...record.activities].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
}, [record?.activities])
return [...record.activities]
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
.filter((activity) => {
if (filters?.host) return activity.entity.host === filters.host
return true
})
}, [record?.activities, filters?.host])

return {
activities,
isLoading,
}
}

export const addReceivedActivity = async (
agent: EasyPIDAppAgent,
input: {
did: string
name: string
domain?: string
logo?: DisplayImage
backgroundColor?: string
credentialIds: string[]
}
) => {
await activityStorage.addActivity(agent, {
id: utils.uuid(),
date: new Date().toISOString(),
type: 'received',
status: 'success',
entity: {
did: input.did,
name: input.name,
host: input.domain ? input.domain : (getHostNameFromUrl(input.did) as string),
logo: input.logo,
backgroundColor: input.backgroundColor,
},
credentialIds: input.credentialIds,
} as IssuanceActivity)
}

export const addSharedActivity = async (
agent: EasyPIDAppAgent,
input: {
status: ActivityStatus
entity: {
did: string
name?: string
logo?: DisplayImage
}
request: {
credentials: Array<{
id: string
disclosedAttributes: string[]
disclosedPayload: Record<string, unknown>
}>
name?: string
purpose?: string
failureReason?: SharingFailureReason
}
}
) => {
await activityStorage.addActivity(agent, {
id: utils.uuid(),
date: new Date().toISOString(),
type: 'shared',
status: input.status,
entity: {
did: input.entity.did,
name: input.entity.name,
host: getHostNameFromUrl(input.entity.did) as string,
logo: input.entity.logo,
},
request: {
name: input.request.name,
purpose: input.request.purpose,
credentials: input.request.credentials,
failureReason: input.request.failureReason,
},
} as PresentationActivity)
}
Loading

0 comments on commit 108ebb1

Please sign in to comment.