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: lifecycle states #206

Merged
merged 3 commits into from
Nov 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
12 changes: 12 additions & 0 deletions apps/easypid/src/features/activity/FunkeActivityDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ export function FunkeActivityDetailScreen() {
/>
)

const isExpired = credential.metadata.validUntil
? new Date(credential.metadata.validUntil) < new Date()
: false

const isNotYetActive = credential.metadata.validFrom
? new Date(credential.metadata.validFrom) > new Date()
: false

if (isPidCredential(credential.metadata.type)) {
return (
<CardWithAttributes
Expand All @@ -111,6 +119,8 @@ export function FunkeActivityDetailScreen() {
activityCredential?.disclosedPayload ?? {},
credential?.claimFormat as ClaimFormat.SdJwtVc | ClaimFormat.MsoMdoc
)}
isExpired={isExpired}
isNotYetActive={isNotYetActive}
/>
)
}
Expand All @@ -128,6 +138,8 @@ export function FunkeActivityDetailScreen() {
disclosedAttributes={activityCredential.disclosedAttributes ?? []}
disclosedPayload={activityCredential.disclosedPayload ?? {}}
disableNavigation={activity.status !== 'success'}
isExpired={isExpired}
isNotYetActive={isNotYetActive}
/>
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export function RequestedAttributesSection({ submission }: RequestedAttributesSe
credential?.disclosedPayload ?? {},
credential?.claimFormat as ClaimFormat.SdJwtVc | ClaimFormat.MsoMdoc
)}
isExpired={
credential.metadata?.validUntil ? new Date(credential.metadata.validUntil) < new Date() : false
}
isNotYetActive={
credential.metadata?.validFrom ? new Date(credential.metadata.validFrom) > new Date() : false
}
/>
)
}
Expand All @@ -61,6 +67,12 @@ export function RequestedAttributesSection({ submission }: RequestedAttributesSe
textColor={credential.textColor}
disclosedAttributes={credential.requestedAttributes ?? []}
disclosedPayload={credential?.disclosedPayload ?? {}}
isExpired={
credential.metadata?.validUntil ? new Date(credential.metadata.validUntil) < new Date() : false
}
isNotYetActive={
credential.metadata?.validFrom ? new Date(credential.metadata.validFrom) > new Date() : false
}
/>
)
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function FunkeCredentialDetailScreen() {
</Paragraph>
</Stack>
<YStack w="100%" gap="$2">
<CardInfoLifecycle />
<CardInfoLifecycle validFrom={credential.validFrom} validUntil={credential.validUntil} />
<InfoButton
variant="view"
title="Card attributes"
Expand Down
15 changes: 15 additions & 0 deletions packages/agent/src/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,11 @@ export function filterAndMapSdJwtKeys(sdJwtVcPayload: Record<string, unknown>) {
Object.entries(visibleProperties).map(([key, value]) => [key, recursivelyMapAttribues(value)])
),
metadata: credentialMetadata,
raw: {
issuedAt: iat ? new Date(iat * 1000) : undefined,
validUntil: exp ? new Date(exp * 1000) : undefined,
validFrom: nbf ? new Date(nbf * 1000) : undefined,
},
}
}

Expand Down Expand Up @@ -417,6 +422,8 @@ export function getCredentialForDisplay(credentialRecord: W3cCredentialRecord |
attributes: mapped.visibleProperties,
metadata: mapped.metadata,
claimFormat: ClaimFormat.SdJwtVc,
validUntil: mapped.raw.validUntil,
validFrom: mapped.raw.validFrom,
}
}
if (credentialRecord instanceof MdocRecord) {
Expand Down Expand Up @@ -446,6 +453,8 @@ export function getCredentialForDisplay(credentialRecord: W3cCredentialRecord |
type: mdocInstance.docType,
} satisfies CredentialMetadata,
claimFormat: ClaimFormat.MsoMdoc,
validUntil: mdocInstance.validityInfo.validUntil,
validFrom: mdocInstance.validityInfo.validFrom,
}
}

Expand Down Expand Up @@ -484,6 +493,12 @@ export function getCredentialForDisplay(credentialRecord: W3cCredentialRecord |
validFrom: undefined,
} satisfies CredentialMetadata,
claimFormat: credentialRecord.credential.claimFormat,
validUntil: credentialRecord.credential.expirationDate
janrtvld marked this conversation as resolved.
Show resolved Hide resolved
? new Date(credentialRecord.credential.expirationDate)
: undefined,
janrtvld marked this conversation as resolved.
Show resolved Hide resolved
validFrom: credentialRecord.credential.issuanceDate
? new Date(credentialRecord.credential.issuanceDate)
: undefined,
}
}

Expand Down
21 changes: 21 additions & 0 deletions packages/app/src/components/BlurBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Stack } from '@package/ui'
import { Paragraph } from '@package/ui/src/base/Paragraph'
import { BlurView } from 'expo-blur'
import { StyleSheet } from 'react-native'

interface BlurBadgeProps {
label: string
color?: string
tint?: 'light' | 'dark'
}

export function BlurBadge({ label, color, tint = 'light' }: BlurBadgeProps) {
return (
<Stack overflow="hidden" bg="#0000001A" br="$12" ai="center" gap="$2">
<BlurView intensity={20} tint={tint} style={StyleSheet.absoluteFillObject} />
<Paragraph variant="caption" opacity={0.8} px="$2.5" py="$0.5" color={color ? color : 'white'}>
{label}
</Paragraph>
</Stack>
)
}
179 changes: 170 additions & 9 deletions packages/app/src/components/CardInfoLifecycle.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,192 @@
import { InfoButton, InfoSheet } from '@package/ui'
import React from 'react'
import { Button, HeroIcons, InfoButton, InfoSheet, Stack, useToastController } from '@package/ui'
import type { StatusVariant } from '@package/ui/src/utils/variants'
import { formatDate, formatDaysString, getDaysUntil } from '@package/utils/src'
import React, { useEffect, useMemo } from 'react'
import { useState } from 'react'
import { useHaptics } from '../hooks/useHaptics'

export function CardInfoLifecycle() {
// Expired requires a different flow (see component below)
type BaseLifeCycle = 'active' | 'revoked' | 'batch'

type LifeCycleContent = {
variant: StatusVariant
title: string
description: string
sheetDescription: string
}

const cardInfoLifecycleVariant: Record<BaseLifeCycle, LifeCycleContent> = {
active: {
variant: 'positive',
title: 'Card is active',
description: 'No actions required',
sheetDescription: 'Your credentials may expire or require an active internet connection to validate.',
},
revoked: {
variant: 'danger',
title: 'Card revoked',
description: 'Card not usable anymore',
sheetDescription:
'The issuer has revoked this card and it can not be used anymore. Contact the issuer for more information.',
},
// We can hardcode this to the rules for the PID credential as this will be the only of this type for now.
batch: {
variant: 'warning',
title: 'Limited card usage',
description: 'verifications left',
sheetDescription:
'This card requires periodic validation using an internet connection. When usage is low you will be notified.',
},
}

interface CardInfoLifecycleProps {
validUntil?: Date
validFrom?: Date
isRevoked?: boolean
batchLeft?: number
}

export function CardInfoLifecycle({ validUntil, validFrom, isRevoked, batchLeft }: CardInfoLifecycleProps) {
const toast = useToastController()
const [isOpen, setIsOpen] = useState(false)
const { withHaptics } = useHaptics()

const state = useMemo(() => {
if (isRevoked) return 'revoked'
if (batchLeft) return 'batch'

return 'active'
}, [isRevoked, batchLeft])

const onPress = withHaptics(() => setIsOpen(!isOpen))

const onPressValidate = withHaptics(() => {
// Implement navigation to the setup eID card flow.
toast.show('Coming soon', { customData: { preset: 'warning' } })
})

if (validUntil || validFrom) {
return <CardInfoLimitedByDate validUntil={validUntil} validFrom={validFrom} />
}

return (
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 never render if validFrom/until is defined?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, because if the many variants I moved that to a different component. (see line 69).

<>
{batchLeft && batchLeft <= 5 && (
<Stack pb="$4">
<Button.Solid bg="$grey-50" bw="$0.5" borderColor="$grey-200" color="$grey-900" onPress={onPressValidate}>
Refresh card <HeroIcons.ArrowPath ml="$-2" size={20} color="$grey-700" />
Copy link
Member

Choose a reason for hiding this comment

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

So i asked about refresh and they recommended me to use the C" issuance flow which does support refresh tokens.

So the flow would be:

  • authentication using eid card first time. Get batch and refresh token
  • use refresh token in background to retrieve new token and get new batch
  • if refresh token also expires we need to trigger eID card flow.

But this will be a lot better already. So i think the "only x left in batch" should only be shown if we werent't able to automatically get new ones and re-authentication is required. But that's really a fallback.

We have the ReceivePidUseCaseFlow. Maybe if you can add these placeholder methods for now I'll implement these later:

  • tryRetrieveBatchUsingRefreshToken

Then if that method fails because of unauthorized we need to trigger eID flow, or at least show to the user that they only have X verifications left and they need to re-authenticate.

</Button.Solid>
</Stack>
)}
<InfoButton
routingType="modal"
variant={cardInfoLifecycleVariant[state].variant}
title={cardInfoLifecycleVariant[state].title}
description={
batchLeft
? `${batchLeft} ${cardInfoLifecycleVariant[state].description}`
: cardInfoLifecycleVariant[state].description
}
onPress={onPress}
/>
<InfoSheet
isOpen={isOpen}
setIsOpen={setIsOpen}
onClose={onPress}
variant={cardInfoLifecycleVariant[state].variant}
title={cardInfoLifecycleVariant[state].title}
description={cardInfoLifecycleVariant[state].sheetDescription}
/>
</>
)
}

type CardInfoLimitedByDateState = 'not-yet-active' | 'active' | 'will-expire' | 'expired'

function CardInfoLimitedByDate({ validUntil, validFrom }: { validUntil?: Date; validFrom?: Date }) {
const [state, setState] = useState<CardInfoLimitedByDateState>('active')
const [isOpen, setIsOpen] = useState(false)
const { withHaptics } = useHaptics()

const onPress = withHaptics(() => setIsOpen(!isOpen))

useEffect(() => {
if (validUntil && validUntil < new Date()) {
setState('expired')
} else if (validFrom && validFrom > new Date()) {
setState('not-yet-active')
} else if (validUntil && validUntil > new Date()) {
setState('will-expire')
} else {
setState('active')
}
}, [validUntil, validFrom])

const content = getCardInfoLimitedByDateVariant(validUntil, validFrom)[state]

return (
<>
<InfoButton
routingType="modal"
variant="positive"
title="Card is active"
description="No actions required"
variant={content.variant}
title={content.title}
description={content.description}
onPress={onPress}
/>
<InfoSheet
isOpen={isOpen}
setIsOpen={setIsOpen}
onClose={onPress}
variant="positive"
title="Card is active"
description="Your credentials may expire or require an active internet connection to validate."
variant={content.variant}
title={content.title}
description={content.sheetDescription}
/>
</>
)
}

function getCardInfoLimitedByDateVariant(
validUntil?: Date,
validFrom?: Date
): Record<CardInfoLimitedByDateState, LifeCycleContent> {
const daysUntilExpiration = getDaysUntil(validUntil)
const daysUntilActivation = getDaysUntil(validFrom)

const activeDaysString = formatDaysString(daysUntilActivation)
const expiryDaysString = formatDaysString(daysUntilExpiration)

const validityPeriod =
validFrom && validUntil
? `The validity period of this card is from ${formatDate(validFrom)} until ${formatDate(validUntil)}.`
: undefined

const activeString = validFrom && `This card will be active in ${activeDaysString}, on ${formatDate(validFrom)}.`
const expiryString = validUntil && `This card expires in ${expiryDaysString}, on ${formatDate(validUntil)}.`

return {
active: {
variant: 'positive',
title: 'Card is active',
description: 'No actions required',
sheetDescription: 'Some credentials may expire or require an active internet connection to validate',
},
expired: {
variant: 'default',
title: 'Card expired',
description: 'The expiration date of this card has passed',
sheetDescription: `The expiration date of this card has passed on ${validUntil?.toLocaleDateString()}.`,
},
'not-yet-active': {
variant: 'default',
title: 'Card not active',
description: `Will be active in ${activeDaysString}`,
sheetDescription: (validityPeriod ?? activeString) as string,
},
'will-expire': {
variant: 'warning',
title: 'Card will expire',
description: `Expires in ${expiryDaysString}`,
sheetDescription: (validityPeriod ?? expiryString) as string,
},
}
}
Loading