Skip to content

Commit

Permalink
feat: lifecycle states (#206)
Browse files Browse the repository at this point in the history
Signed-off-by: Jan <[email protected]>
  • Loading branch information
janrtvld authored Nov 11, 2024
1 parent 7f41611 commit b1145a3
Show file tree
Hide file tree
Showing 12 changed files with 284 additions and 17 deletions.
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
? new Date(credentialRecord.credential.expirationDate)
: undefined,
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 (
<>
{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" />
</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

0 comments on commit b1145a3

Please sign in to comment.