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

fix: fixed attributes count and ui improvements #186

Merged
merged 7 commits into from
Oct 11, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function Screen() {
<FunkeRequestedAttributesDetailScreen
id={id}
disclosedPayload={JSON.parse(disclosedPayload)}
disclosedAttributeLength={disclosedAttributeLength}
disclosedAttributeLength={Number.parseInt(disclosedAttributeLength)}
/>
)
}
3 changes: 1 addition & 2 deletions apps/easypid/src/crypto/bPrime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,6 @@ export const requestSdJwtVcFromSeedCredential = async ({

const tokenResponse = await agent.modules.openId4VcHolder.requestToken({
resolvedCredentialOffer,
// @ts-ignore
dPopKeyJwk: P256Jwk.fromJson(deviceKeyPair.asJwk() as unknown as JwkJson),
getCreateJwtCallback: getCreateJwtCallbackForBPrime,
customBody: {
Expand Down Expand Up @@ -408,7 +407,6 @@ export const requestSdJwtVcFromSeedCredential = async ({
}
},
...tokenResponse,
// @ts-ignore
additionalCredentialRequestPayloadClaims: {
verifier_ka: rpEphPub,
},
Expand Down Expand Up @@ -549,4 +547,5 @@ export const convertAndStorePidDataIntoFakeSdJwtVc = async (
})
setOpenId4VcCredentialMetadata(record, openId4VcMetadata)
await storeCredential(agent, sdJwtVcRecord)
return sdJwtVcRecord
}
82 changes: 53 additions & 29 deletions apps/easypid/src/features/activity/FunkeActivityDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import { FlexPage, Heading, HeroIcons, IdCard, Paragraph, ScrollView, Spacer, Stack, YStack } from '@package/ui'
import { FlexPage, Heading, HeroIcons, Paragraph, ScrollView, Stack, YStack } from '@package/ui'
import React from 'react'
import { createParam } from 'solito'

import { CredentialAttributes, TextBackButton, activityTitleMap } from '@package/app'
import { TextBackButton, activityTitleMap } from '@package/app'
import { useScrollViewPosition } from '@package/app/src/hooks'
import { useCredentialsForDisplay } from 'packages/agent/src'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useRouter } from 'solito/router'
import germanIssuerImage from '../../../assets/german-issuer-image.png'
import { CardWithAttributes } from '../share/components/RequestedAttributesSection'
import { useActivities } from './activityRecord'

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

// When it's a credential, it should render a credential detail screen.
// As we only have the PID credential this is currently not needed to implement.
// So the activity detail screen is always a 'shared data' screen.

export function FunkeActivityDetailScreen() {
const { params } = useParams()
const router = useRouter()
const { bottom } = useSafeAreaInsets()

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

if (!activity) {
if (!activity || activity.type === 'received') {
// Received activity should route to credential detail (until support for multiple credentials in one request)
router.back()
return
}
Expand All @@ -38,27 +37,52 @@ export function FunkeActivityDetailScreen() {
<HeroIcons.ShieldCheckFilled strokeWidth={2} color="$positive-500" size={56} />
</YStack>
<YStack gap="$4" px="$4" marginBottom={bottom}>
{activity.disclosedPayload ? (
<>
<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>
<CredentialAttributes disableHeader subject={activity.disclosedPayload} headerStyle="small" />
</>
) : (
<Paragraph>Disclosed information could not be shown.</Paragraph>
)}
<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))
Copy link
Member

Choose a reason for hiding this comment

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

this will break if you remove the credential is that what we want? We could still show the disclosedPayload i think? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added that when the credential is not found it will still show the card but just with the name 'Unknown credential' and default branding. So now it will still show the requested attrs and disclosed payload when you've deleted the credential.

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>
)}
</Stack>
</YStack>
</ScrollView>
<YStack btw="$0.5" borderColor="$grey-200" pt="$4" mx="$-4" px="$4" bg="$background">
Expand Down
3 changes: 2 additions & 1 deletion apps/easypid/src/features/activity/FunkeActivityScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export function FunkeActivityScreen() {
subtitle={activity.entityName ?? activity.entityHost}
date={new Date(activity.date)}
type={activity.type}
credentialId={activity.credentialId}
// FIXME: Handle multiple credentials received in one request
Copy link
Member

Choose a reason for hiding this comment

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

we could also store them as separate activities if that's simpler?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It works fine, there is just no support for receiving multiple credentials in 1 request atm so will do this later.

credentialId={activity.type === 'received' ? activity.credentialIds?.[0] : undefined}
Copy link
Member

Choose a reason for hiding this comment

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

what happens if you delete the cred?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It routes to the credential detail screen atm, so it will just toast you that the credential can not be found.

/>
))}
</React.Fragment>
Expand Down
28 changes: 17 additions & 11 deletions apps/easypid/src/features/activity/activityRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,30 @@ import { useMemo } from 'react'

export type ActivityType = 'shared' | 'received'

interface Activity {
interface BaseActivity {
id: string
type: ActivityType
date: string
disclosedPayload?: Record<string, unknown>

// only relevant for received activity
credentialId?: string

// host of the entity interacted with
// e.g. funke.animo.id
entityHost: string

// name of the entity interacted with
// e.g. Animo Solutions
entityName?: string
}

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

interface IssuanceActivity extends BaseActivity {
type: 'received'
credentialIds: string[]
}

export type Activity = PresentationActivity | IssuanceActivity

interface ActivityRecord {
activities: Activity[]
}
Expand Down
24 changes: 2 additions & 22 deletions apps/easypid/src/features/onboarding/onboardingContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -693,8 +693,6 @@ export function OnboardingContextProvider({

for (const credential of credentials) {
if (credential instanceof SdJwtVcRecord) {
await storeCredential(secureUnlock.context.agent, credential)

const parsed = secureUnlock.context.agent.sdJwtVc.fromCompact<SdJwtVcHeader, PidSdJwtVcAttributes>(
credential.compactSdJwtVc
)
Expand All @@ -711,32 +709,14 @@ export function OnboardingContextProvider({
date: new Date().toISOString(),
entityHost: getHostNameFromUrl(parsed.prettyClaims.iss) as string,
entityName: issuerName,
credentialId: credential.id,
credentialIds: [credential.id],
})
} /* else if (credential instanceof MdocRecord) {
await storeCredential(secureUnlock.context.agent, credential)

// NOTE: we don't set the userName here as we always get SD-JWT VC and MODC at the same time currently
// so it should be set
} */ else {
const payload = credential.credential.split('.')[1]
const {
iss,
pid_data: { given_name, family_name },
} = JSON.parse(TypedArrayEncoder.fromBase64(payload).toString())
setUserName(
`${capitalizeFirstLetter(given_name.toLowerCase())} ${capitalizeFirstLetter(family_name.toLowerCase())}`
)

const issuerName = credential.openId4VcMetadata.issuer.display?.[0]?.name
await activityStorage.addActivity(secureUnlock.context.agent, {
id: utils.uuid(),
type: 'received',
date: new Date().toISOString(),
entityHost: getHostNameFromUrl(iss) as string,
entityName: issuerName,
})
}
} */
}

setCurrentStepName('id-card-complete')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function OnboardingIdCardRequestedAttributes({
backgroundImage={display?.backgroundImage}
backgroundColor={display?.backgroundColor}
disclosedAttributes={requestedAttributes.map((a) => sanitizeString(a))}
disableNavigation
/>
</YStack>
</YStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function FunkeOpenIdCredentialNotificationScreen() {
date: new Date().toISOString(),
entityHost: getHostNameFromUrl(metadata.issuer) as string,
entityName: display.issuer.name,
credentialId: credentialRecord.id,
credentialIds: [credentialRecord.id],
})

setIsAccepted(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const OfferCredentialSlide = ({
const { display, attributes } = getCredentialForDisplay(credentialRecord)

const handleAccept = async () => {
await onAccept().then(completeProgressBar)
await onAccept()
}

const handleDecline = () => {
Expand Down Expand Up @@ -132,8 +132,9 @@ export const OfferCredentialSlide = ({
useEffect(() => {
if (isAccepted && isAllowedToComplete) {
setIsCompleted(true)
completeProgressBar()
}
}, [isAccepted, isAllowedToComplete])
}, [isAccepted, isAllowedToComplete, completeProgressBar])

return (
<YStack fg={1} jc="space-between">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
} from '@easypid/crypto/bPrime'
import { useSeedCredentialPidData } from '@easypid/storage'
import { usePushToWallet } from '@package/app/src/hooks/usePushToWallet'
import { getHostNameFromUrl } from 'packages/utils/src/url'
import { getPidAttributesForDisplay, usePidCredential } from '../../hooks'
import { activityStorage } from '../activity/activityRecord'
import { FunkePresentationNotificationScreen } from './FunkePresentationNotificationScreen'
Expand Down Expand Up @@ -127,20 +126,29 @@ export function FunkeOpenIdPresentationNotificationScreen() {
allowUntrustedCertificate: true,
})

const credential = submission.entries[0]?.credentials[0]
const disclosedPayload =
credential?.metadata?.type === pidCredential?.type
? getPidAttributesForDisplay(
credential.disclosedPayload ?? {},
credential.metadata ?? ({} as CredentialMetadata),
credential.claimFormat as ClaimFormat.SdJwtVc /* | ClaimFormat.MsoMdoc */
)
: credential?.disclosedPayload
const credentialsWithDisclosedPayload = submission.entries.flatMap((entry) => {
return entry.credentials.map((credential) => {
const disclosedPayload =
credential.metadata?.type === pidCredential?.type
? getPidAttributesForDisplay(
credential.disclosedPayload ?? {},
credential.metadata ?? ({} as CredentialMetadata),
credential.claimFormat as ClaimFormat.SdJwtVc /* | ClaimFormat.MsoMdoc */
)
: credential.disclosedPayload

return {
id: credential.id,
disclosedAttributes: credential.requestedAttributes ?? [],
disclosedPayload,
}
})
})

await activityStorage.addActivity(agent, {
id: utils.uuid(),
type: 'shared',
disclosedPayload,
credentials: credentialsWithDisclosedPayload,
date: new Date().toISOString(),
entityHost: credentialsForRequest.verifierHostName as string,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'
interface FunkeRequestedAttributesDetailScreenProps {
id: string
disclosedPayload: Record<string, unknown>
disclosedAttributeLength: string
disclosedAttributeLength: number
}

export function FunkeRequestedAttributesDetailScreen({
Expand Down Expand Up @@ -68,7 +68,8 @@ export function FunkeRequestedAttributesDetailScreen({
/>
<Stack g="md">
<Heading variant="h1">
{disclosedAttributeLength} attributes from {activeCredential?.display.name}
{disclosedAttributeLength} attribute{disclosedAttributeLength > 1 ? 's' : ''} from{' '}
{activeCredential?.display.name}
</Heading>
{activeCredential?.display.issuer && (
<Paragraph color="$grey-700">Issued by {activeCredential?.display.issuer.name}</Paragraph>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function RequestedAttributesSection({ submission }: RequestedAttributesSe
<YStack gap="$4">
<YStack gap="$2">
<Circle size="$2.5" mb="$2" backgroundColor="$primary-500">
<HeroIcons.CircleStack color="$white" size={18} />
<HeroIcons.CircleStackFilled color="$white" size={16} />
</Circle>
<Heading variant="h3" fontWeight="$semiBold">
Requested data
Expand Down Expand Up @@ -85,13 +85,15 @@ export function CardWithAttributes({
backgroundImage,
disclosedAttributes,
disclosedPayload,
disableNavigation = false,
}: {
id: string
name: string
backgroundColor?: string
backgroundImage?: DisplayImage
disclosedAttributes: string[]
disclosedPayload?: Record<string, unknown>
disableNavigation?: boolean
}) {
const router = useRouter()
const hasInternet = useHasInternetConnection()
Expand All @@ -110,12 +112,20 @@ export function CardWithAttributes({

const onPress = () => {
router.push(
`/credentials/requestedAttributes?id=${id}&disclosedPayload=${encodeURIComponent(JSON.stringify(disclosedPayload ?? {}))}&disclosedAttributeLength=${filteredDisclosedAttributes.length}`
`/credentials/requestedAttributes?id=${id}&disclosedPayload=${encodeURIComponent(
JSON.stringify(disclosedPayload ?? {})
)}&disclosedAttributeLength=${filteredDisclosedAttributes?.length}`
)
}

return (
<Card br="$6" borderWidth="$0.5" borderColor="$borderTranslucent" overflow="hidden" onPress={onPress}>
<Card
br="$6"
borderWidth="$0.5"
borderColor="$borderTranslucent"
overflow="hidden"
onPress={disableNavigation ? undefined : onPress}
>
<Stack p="$5" pos="relative" bg={backgroundColor ?? '$grey-900'}>
{hasInternet && backgroundImage?.url && (
<Stack pos="absolute" top={0} left={0} right={0} bottom={0}>
Expand Down Expand Up @@ -148,7 +158,7 @@ export function CardWithAttributes({
</Stack>
</XStack>
))}
{disclosedPayload && (
{!disableNavigation && disclosedPayload && (
<Stack pos="absolute" bottom="$0" right="$0">
<IconContainer onPress={onPress} icon={<HeroIcons.ArrowRight />} />
</Stack>
Expand Down
Loading