diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cd06832..dc1bd512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # verifier-plus Changelog +## 2.0.2 +### Added +- Contextual Help +### Fixed +- browser title + ## 2.0.1 ### Added - Add Alignments to Open Badges 3.0 Display diff --git a/app/components/Alignment/Alignment.tsx b/app/components/Alignment/Alignment.tsx index cef88a9d..13feafd2 100644 --- a/app/components/Alignment/Alignment.tsx +++ b/app/components/Alignment/Alignment.tsx @@ -4,6 +4,8 @@ import type { Alignment as AlignmentType } from '@/types/credential.d' import { isValidHttpUrl } from '@/lib/url' import type { AlignmentProps } from './Alignment.d' import { TestId } from '@/lib/testIds' +import { alignmentHelpDescription, alignmentHelpSections } from '../Help' +import { ContextualHelp } from '../ContextualHelp/ContextualHelp' export const Alignment = ({ alignment, headerClassName }: AlignmentProps) => { const alignmentsArray: AlignmentType[] = Array.isArray(alignment) ? alignment : (alignment ? [alignment] : []) @@ -18,7 +20,7 @@ export const Alignment = ({ alignment, headerClassName }: AlignmentProps) => { return (
-

Alignments

+

Alignments

+
) } diff --git a/app/components/Button/Button.d.ts b/app/components/Button/Button.d.ts index 18798cff..8b4557c7 100644 --- a/app/components/Button/Button.d.ts +++ b/app/components/Button/Button.d.ts @@ -6,4 +6,5 @@ export type ButtonProps = { icon?: ReactElement | null; onClick?: (e: React.SyntheticEvent) => void; className?: string; + children?: ReactElement; } diff --git a/app/components/Button/Button.tsx b/app/components/Button/Button.tsx index acc4dff0..dd93048b 100644 --- a/app/components/Button/Button.tsx +++ b/app/components/Button/Button.tsx @@ -1,9 +1,9 @@ import type { ButtonProps } from './Button.d'; import styles from './Button.module.css'; -export const Button = ({secondary = false, onClick = () => {}, text, icon = null, className = ''}: ButtonProps) => { +export const Button = ({secondary = false, onClick = () => {}, text, icon = null, className = '', children}: ButtonProps) => { return ( ) } \ No newline at end of file diff --git a/app/components/ContextualHelp/ContextualHelp.d.ts b/app/components/ContextualHelp/ContextualHelp.d.ts new file mode 100644 index 00000000..2c74d971 --- /dev/null +++ b/app/components/ContextualHelp/ContextualHelp.d.ts @@ -0,0 +1,16 @@ +import React, { ReactElement, ReactNode } from "react" + +export type ContextualHelpProps = { + title?: string; + description?: ReactNode; + children?: ReactNode; + iconSize?: string; + iconColor?: string; + style?: string; + sections?: CollapsibleSectionProps[] +} + +export type CollapsibleSectionProps = { + sectionTitle: string; + content: ReactNode; +} \ No newline at end of file diff --git a/app/components/ContextualHelp/ContextualHelp.module.css b/app/components/ContextualHelp/ContextualHelp.module.css new file mode 100644 index 00000000..68545eb4 --- /dev/null +++ b/app/components/ContextualHelp/ContextualHelp.module.css @@ -0,0 +1,20 @@ +.icon { + margin-left: .5em; + font-size: .9em; + vertical-align: top; +} + + +.help-icon-container { + /* Ensures the cursor changes to a hand icon on hover */ + cursor: pointer; + /* Optional: Add a smooth transition for visual effects */ + transition: color 0.3s ease; +} + +.help-icon-container:hover { + /* Optional: Change the color on hover */ + color: #007bff; + /* Optional: Add a slight scale effect */ + transform: scale(1.1); +} \ No newline at end of file diff --git a/app/components/ContextualHelp/ContextualHelp.tsx b/app/components/ContextualHelp/ContextualHelp.tsx new file mode 100644 index 00000000..2d422183 --- /dev/null +++ b/app/components/ContextualHelp/ContextualHelp.tsx @@ -0,0 +1,80 @@ +import * as React from 'react'; +import type { CollapsibleSectionProps, ContextualHelpProps } from './ContextualHelp.d'; +import HelpIcon from '@mui/icons-material/HelpOutlined'; +import { Dialog } from '@base-ui-components/react/dialog'; +import { ScrollArea } from '@base-ui-components/react/scroll-area'; +import { Collapsible } from '@base-ui-components/react/collapsible'; +import { useHelpContext } from '@/lib/HelpContext'; + +function ChevronIcon(props: React.ComponentProps<'svg'>) { + return ( + + + + ); +} + +const CollapsibleSection = ({ sectionTitle, content }: CollapsibleSectionProps) => { + return ( + + + + {sectionTitle} + + +
+ {content} +
+
+
) +} + +export const ContextualHelp = ({ title, description, iconSize = '12px', children, sections, iconColor = 'info', style = "align-top mx-1 inline m-h-1" }: ContextualHelpProps) => { + let {isHelpEnabled} = useHelpContext(); + if (isHelpEnabled) { + return ( + + + e.stopPropagation()} nativeButton={false} render={}> + + + + + {title} + + + +
+ {description} + {children} + { + sections && sections.map((section : CollapsibleSectionProps) => ( + + )) + } +
+
+ + + +
+ +
+ + Close + +
+
+
+
+ ); + } else { + return null + } +} + + + + + + diff --git a/app/components/CopyToClipboard/CopyToClipboard.d.ts b/app/components/CopyToClipboard/CopyToClipboard.d.ts new file mode 100644 index 00000000..ea9ce593 --- /dev/null +++ b/app/components/CopyToClipboard/CopyToClipboard.d.ts @@ -0,0 +1,9 @@ +export type CopyToClipboardProps = { + text: string; + buttonText: string; +} + +export type CopyButtonProps = { + buttonText: string; + handleClick: function; +} diff --git a/app/components/CopyToClipboard/CopyToClipboard.tsx b/app/components/CopyToClipboard/CopyToClipboard.tsx new file mode 100644 index 00000000..a315acf7 --- /dev/null +++ b/app/components/CopyToClipboard/CopyToClipboard.tsx @@ -0,0 +1,35 @@ +import type { CopyButtonProps, CopyToClipboardProps } from './CopyToClipboard.d'; + + +import { IconButton, Button, Snackbar } from '@mui/material' +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { useState } from 'react' + +const CopyButton = ({buttonText, handleClick} : CopyButtonProps) => { + return buttonText? + + : + +} + +export const CopyToClipboard = ({text, buttonText}: CopyToClipboardProps) => { + const [open, setOpen] = useState(false) + + const handleClick = () => { + setOpen(true) + navigator.clipboard.writeText(text) + } + + return ( + <> + + setOpen(false)} + autoHideDuration={2500} + message="Copied to clipboard!" + /> + + ) +} + diff --git a/app/components/CredentialCard/CredentialCard.tsx b/app/components/CredentialCard/CredentialCard.tsx index 27e86629..3dd71e2e 100644 --- a/app/components/CredentialCard/CredentialCard.tsx +++ b/app/components/CredentialCard/CredentialCard.tsx @@ -15,6 +15,10 @@ import { extractNameFromOBV3Identifier } from '@/lib/extractNameFromOBV3Identifi import { TestId } from '@/lib/testIds'; import { Alignment } from '@/components/Alignment/Alignment'; +import { ContextualHelp } from '@/components/ContextualHelp/ContextualHelp' +import { holderHelpDescription, holderHelpSections, titleHelpDescription, titleHelpSections, achievementTypeHelpDescription, achievementTypeHelpSections, validUntilHelpSections, validUntilHelpDescription, validFromHelpDescription, validFromHelpSections, descriptionHelpDescription, descriptionHelpSections, criteriaHelpDescription, criteriaHelpSections } from '@/components/Help'; + + export const CredentialCard = ({ credential, wasMulti = false }: CredentialCardProps) => { // TODO: add back IssuerInfoModal @@ -64,8 +68,8 @@ export const CredentialCard = ({ credential, wasMulti = false }: CredentialCardP
{displayValues.achievementImage ? achievement image : null}
-

{displayValues.credentialName}

- {displayValues.achievementType ?

Achievement Type : {displayValues.achievementType}

: null} +

{displayValues.credentialName}

+ {displayValues.achievementType ?

Achievement Type : {displayValues.achievementType}

: null}
@@ -75,11 +79,20 @@ export const CredentialCard = ({ credential, wasMulti = false }: CredentialCardP
{displayValues.issuanceDate && ( - + )} {displayValues.issuedTo ? - + : null } {displayValues.credentialDescription ? - + : null } {displayValues.criteria && (
-

Criteria

- {/*
{displayValues.criteria}
*/} +

Criteria

{displayValues.criteria}
diff --git a/app/components/Help/AchievementTypeHelp/AchievementTypeHelp.tsx b/app/components/Help/AchievementTypeHelp/AchievementTypeHelp.tsx new file mode 100644 index 00000000..3feda7f5 --- /dev/null +++ b/app/components/Help/AchievementTypeHelp/AchievementTypeHelp.tsx @@ -0,0 +1,58 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const ExampleV2Section = () => { + return +} + +const DeterminationSection = () => { + return ( +
+
achievementType is a property of an Open Badges version 3 Verifiable Credential:
+
    +
  • credential.credentialSubject.achievement.achievementType
  • +
+
See the example section for a working example.
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • Examples include 'Award', 'Certification', 'DoctoralDegree'.
  • +
  • A full list of pre-defined Open Badge achievement types is defined in the Open Badges Specification.
  • +
  • New OBv3 achievement types can be added by extending the OBv3 context.
  • +
  • If no achievementType is available, nothing is shown, nor the label.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
The goal of the achievementType field is interoperability. + If different credential issuers use the same achievementType for similar credentials then hopefully consumers of the + credential can treat the credentials as equivalent to some degree. It might of course be less useful to treat + two credentials with a very general achievementType like 'Award' as equivalent, but hopefully moreso for two credential of type 'DoctoralDegree'. Over time + we expect that issuers will converge on common definitions. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
The type of Open Badge v3 achievement.
+ ) +} + +export const achievementTypeHelpDescription = DescriptionSection() + +export const achievementTypeHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the AchievementType', content: DeterminationSection() }, + { sectionTitle: 'Example - Open Badge version 3', content: ExampleV2Section() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] + + diff --git a/app/components/Help/AlignmentHelp/AlignmentHelp.tsx b/app/components/Help/AlignmentHelp/AlignmentHelp.tsx new file mode 100644 index 00000000..91857402 --- /dev/null +++ b/app/components/Help/AlignmentHelp/AlignmentHelp.tsx @@ -0,0 +1,57 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const SampleVCSection = () => { + return +} + +const DeterminationSection = () => { + return ( +
+
If present:
+
    +
  • credential.credentialSubject.achievement.alignment
  • +
+
See the example section for an example
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • A list of alignments with published credential definitions.
  • +
  • Can be thought of as a list of equivalencies with other types of credential.
  • +
  • Defined by the OpenBadges version 3 data model, but not required.
  • +
  • Not required by Verifiable Credential data model.
  • +
  • If no alignments are provided, nothing is shown, including the 'Alignments' title.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
The alignment entries + can be intended for both human and machine consumption. The 'targetCode' and 'targetUrl' provide machine readable references, and + the 'targetName' and 'targetDescription' provide human readable descriptions. +
+ ) +} + +const DescriptionSection = () => { + return ( +
How the credential aligns with published credential definitions.
+ ) +} + +export const alignmentHelpDescription = DescriptionSection() + +export const alignmentHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine Alignments', content: DeterminationSection() }, + { sectionTitle: 'Example VC - alignments', content: SampleVCSection() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] + + + diff --git a/app/components/Help/ChapiHelp/ChapiHelp.js b/app/components/Help/ChapiHelp/ChapiHelp.js new file mode 100644 index 00000000..1a344105 --- /dev/null +++ b/app/components/Help/ChapiHelp/ChapiHelp.js @@ -0,0 +1,55 @@ + + +import styles from '../Help.module.css'; + +const DetailsSection = () => { + return ( +
    +
  • Request credentials from any web wallet that supports the Credential Handler API (CHAPI)
  • +
  • The web wallet must have been registered with the browser beforehand.
  • +
  • Your web wallet will provide instructions on registering with the browser.
  • +
  • This feature is largely here as an example and for experimentation
  • +
  • You typically aren't going to use the feature unless you are here specifically to experiement with this CHAPI implementation.
  • +
) +} + +const NotesSection = () => { + return ( + <> +
+ + Credential Handler API (CHAPI) is an API that allows the end user to select whatever wallet they'd like to use, + rather than dictating that they must use a specific wallet, as typically happens when deeplinking to a + wallet. +
+
+ You can see an example of deeplinking to a specific wallet (the LCW) with the 'Request Credentials from LCW' button on this page. +
+
+ An alternative to both deeplinking and CHAPI is the Digital Credentials API for which we don't yet + have an example. +
+ + ) +} + + + + +const DescriptionSection = () => { + return ( +
Requests credentials from a wallet using the Credential Handler API (CHAPI).
+ ) +} + +export const chapiHelpDescription = DescriptionSection() + +export const chapiHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'Notes', content: NotesSection() } +] + + + + + diff --git a/app/components/Help/CredentialFormatHelp/CredentialFormatHelp.tsx b/app/components/Help/CredentialFormatHelp/CredentialFormatHelp.tsx new file mode 100644 index 00000000..6c4c2a1d --- /dev/null +++ b/app/components/Help/CredentialFormatHelp/CredentialFormatHelp.tsx @@ -0,0 +1,57 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + + +const SampleVCSection = () => { + return +} + +const DeterminationSection = () => { + return ( +
+
The credential must:
+
    +
  • be valid JSON
  • +
  • have a JSON-LD context
  • +
+
See the example section for an example of a well formed Verifiable Credential.
+ +
) +} + +const DetailsSection = () => { + return ( +
    +
  • Various checks to confirm we are dealing with a well-formed Verifiable Credential.
  • +
  • These aren't checks on the validity of the signature or data, but rather simply that the data is in the format we expect.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
We try to check for as many things as possible, but + of course there may be edge cases that haven't yet surfaced. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
A check that the credential is properly formatted.
+ ) +} + +export const credentialFormatHelpDescription = DescriptionSection() + +export const credentialFormatHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Credential is Properly Formatted', content: DeterminationSection() }, + { sectionTitle: 'Example VC', content: SampleVCSection() }, + { sectionTitle: 'Notes', content: NotesSection() } +] + + + diff --git a/app/components/Help/CriteriaHelp/CriteriaHelp.tsx b/app/components/Help/CriteriaHelp/CriteriaHelp.tsx new file mode 100644 index 00000000..56449d82 --- /dev/null +++ b/app/components/Help/CriteriaHelp/CriteriaHelp.tsx @@ -0,0 +1,61 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + + + + +const SampleVCSection = () => { + return +} + +const DeterminationSection = () => { + return ( +
+
In order of preference:
+
    +
  • credential.credentialSubject.achievement.criteria.narrative (OBv3)
  • +
  • credential.credentialSubject.hasCredential.competencyRequired
  • +
+
See the example section for an example of the OBv3 encoding. The hasCredential.competencyRequired encoding is a legacy encoding and should no longer be used.
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • A human readable description of the criteria that must be satisfied to earn the credential.
  • +
  • Required by the OpenBadges version 3 data model.
  • +
  • Not required by Verifiable Credential data model.
  • +
  • If no criteria are provided, nothing is shown, including the 'Criteria' title.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
The criteria field supports + Markdown syntax. +
+
The hasCredential.competencyRequired encoding is a legacy encoding and should no longer be used.
+ + ) +} + +const DescriptionSection = () => { + return ( +
A human readable description of the criteria that must be satisfied to earn the credential.
+ ) +} + +export const criteriaHelpDescription = DescriptionSection() + +export const criteriaHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Criteria', content: DeterminationSection() }, + { sectionTitle: 'Example VC - credential criteria', content: SampleVCSection() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] + + + diff --git a/app/components/Help/DescriptionHelp/DescriptionHelp.tsx b/app/components/Help/DescriptionHelp/DescriptionHelp.tsx new file mode 100644 index 00000000..0bb0635f --- /dev/null +++ b/app/components/Help/DescriptionHelp/DescriptionHelp.tsx @@ -0,0 +1,68 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + + + + +const SampleVCSection = () => { + return +} + + + +const DeterminationSection = () => { + return ( +
+
In order of preference:
+
    +
  • credential.credentialSubject.achievement.description (OBv3)
  • +
  • credential.credentialSubject.hasCredential.description
  • +
+
See the example section for an example of the OBv3 encoding. The other encoding is a legacy encoding that will likely be deprecated.
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • A short human readable description of the credential
    e.g, 'Bachelor of Science in Computer Science'.
  • +
  • Required by the OpenBadges version 3 data model.
  • +
  • Not required by the Verifiable Credential data model.
  • +
  • If no description is provided, nothing is shown, including the 'Description' title.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
Although we do support 'credentialSubject.hasCredential.description', + that is an older encoding that we only support for backwards compatability and anyone authoring new credentials should likely + not use that encoding. Support will likely be removed in the near future. + +
+
Note that formatting is not accommodated in the + description field. To provide a more stylized presentation, consider using the 'Criteria' field, which does allow + Markdown syntax. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
A short human readable description of the credential.
+ ) +} + +export const descriptionHelpDescription = DescriptionSection() + +export const descriptionHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Description', content: DeterminationSection() }, + { sectionTitle: 'Example VC - credential description', content: SampleVCSection() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] + + + diff --git a/app/components/Help/DragAndDropHelp/DragAndDropHelp.js b/app/components/Help/DragAndDropHelp/DragAndDropHelp.js new file mode 100644 index 00000000..860346aa --- /dev/null +++ b/app/components/Help/DragAndDropHelp/DragAndDropHelp.js @@ -0,0 +1,49 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const ExampleV2Section = () => { + return +} + +const ExampleV1Section = () => { + return +} + +const DetailsSection = () => { + return ( +
    +
  • You can either choose the file from your computer by clicking 'browse', or find the file on your computer and drag it into the dotted box.
  • +
  • The file must contain a Verifiable Credential.
  • +
  • Verifiable Credentials are encoded as JSON and the file extension is typically '.json'.
  • +
  • Examples of Verifiable Credentials are provided below in the examples sections.
  • +
  • You can alternatively paste the JSON (or a link to the JSON) in the text area above labelled 'Paste JSON or URL'.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
VerifierPlus only verifies Verifiable Credentials, which + includes Open Badges version 3, but not Open Badges version 2. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
Drag in a Verifiable Credential file or click 'browse' to select the file.
+ ) +} + +export const dragAndDropHelpDescription = DescriptionSection() + +export const dragAndDropHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'Example - Verifiable Credential v1', content: ExampleV1Section() }, + { sectionTitle: 'Example - Verifiable Credential v2', content: ExampleV2Section() }, + { sectionTitle: 'Notes', content: NotesSection() } +] + + diff --git a/app/components/Help/Help.module.css b/app/components/Help/Help.module.css new file mode 100644 index 00000000..52f0d067 --- /dev/null +++ b/app/components/Help/Help.module.css @@ -0,0 +1,103 @@ +.note { + color: white; + font-size: 16px; + background-color: gray; + padding: 10px; + margin-top: 10px; + margin-left: 20px; + margin-right: 20px; + display: inline-block; + border-radius: 4px; +} + +.list { + list-style-type: disc; + padding-left: 20px; +} + +.image { + display: inline-block +} + +.title { + text-align: center; + font-size: 18px; + font-weight: 500; +} + +.criteriaTitle { + text-align: center; + font-size: 16px; + font-weight: 600; + margin-bottom: 15px; +} + +.subtitle { + font-size: 15px; + font-weight: 500; + padding: 5px; +} + +.criteria { + background-color: aliceblue; + padding: 5px +} + +.preference { + padding-bottom: 5px; + padding-top: 5px; +} + +.infoIcon { + display: inline-block; + padding-right: 6px; + width: 22px; + height: 22px; + vertical-align: top; +} + +.scrollableCode { + overflow-x: scroll; + padding-top: 20px; + padding-bottom: 20px; + margin-bottom: 20px; +} + +.scrollableLink { + overflow-x: scroll; + padding-top: 10px; + padding-bottom: 10px; + color: black; + display: block; + white-space: nowrap; +} + +.qrCode { + display: block; + margin: 0 auto; + width: 30%; + padding: 20px +} + +.emphasis { + font-weight: 700; +} + +.italics { + font-style:italic; +} + +.externalLink { + color: black; + font-weight: 800; + text-decoration: underline; +} + +.scrollableLink { + overflow-x: scroll; + padding-top: 10px; + padding-bottom: 10px; + color:black; + display:block; + white-space: nowrap; +} \ No newline at end of file diff --git a/app/components/Help/HolderHelp/HolderHelp.tsx b/app/components/Help/HolderHelp/HolderHelp.tsx new file mode 100644 index 00000000..45fa3000 --- /dev/null +++ b/app/components/Help/HolderHelp/HolderHelp.tsx @@ -0,0 +1,86 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + + + + +const CredSubjNameVCSection = () => { + return +} + +const CredSubjIdentityHashVCSection = () => { + return +} + +const CredNameVCSection = () => { + return +} + + + +const DeterminationSection = () => { + return ( +
+
In order of preference:
+
    +
  • credential.credentialSubject.name
  • +
  • credential.credentialSubject.identifier[identityType=name].identityHash
  • +
  • credential.name
  • +
+
See the example sections for examples of each.
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • Typically, this is the subject of the credential, e.g., a student who earned a diploma.
  • +
  • Sometimes called the 'subject', 'earner', 'recipient', or the 'holder'.
  • +
  • Set directly in the Verifiable Credential and cannot be changed without invalidating the cryptographic signature.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
Some Verifiable Credentials are issued to + an identifier rather than the name of a person. In particular, they can be issued to a Decentralized Identifier (DID) + belonging to the holder of the credential. The holder can later use this DID to sign a cryptographic challenge, thereby proving that they + control the credential. +
+
+ A credential might be 'issued to' someone other than the 'subject' of the credential, ans so we might instead say the + credential was issued to a 'holder'. An example could be a birth certificate for a child (the subject), but issued to the parent (the holder) + who can then act on the child's behalf. However, Verifiable Credentials are a relatively new technology, there + isn't yet clear consensus on terminology, and the same term can often be used to describe different things, so it is + important to interpet context. The subject of the credential, however, will always be whomever the underlying credential applies to. +
+ +
Some Verifiable Credentials might not + explicitly define a subject. Such credentials are sometimes called 'bearer credentials' meaning they belong to + whoever holds the credential. As such, when determining what to show for the 'issued to' field we try our best (as described below) + to determine the name of the credential subject, but default in the end to credential.name if no other value is found. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
The subject of the credential.
+ ) +} + +export const holderHelpDescription = DescriptionSection() + +export const holderHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Subject of the credential', content: DeterminationSection() }, + { sectionTitle: 'Example - credentialSubject.name', content: CredSubjNameVCSection() }, + { sectionTitle: 'Example - credentialSubject.identifier.identityHash', content: CredSubjIdentityHashVCSection() }, + { sectionTitle: 'Example - credential name', content: CredNameVCSection() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] + + diff --git a/app/components/Help/IssuerDetailsHelp/IssuerDetailsHelp.js b/app/components/Help/IssuerDetailsHelp/IssuerDetailsHelp.js new file mode 100644 index 00000000..aa7c5655 --- /dev/null +++ b/app/components/Help/IssuerDetailsHelp/IssuerDetailsHelp.js @@ -0,0 +1,75 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const ExampleNoImageSection = () => { + return +} + +const ExampleImageSection = () => { + return +} + +const ExampleImageIdSection = () => { + return +} + +const DeterminationSection = () => { + return ( + <> +
In order of preference:
+
    +
  • If the issuer is listed in a registry, we pull the issuer name, logo, and url from the registry.
  • +
  • If the issuer is listed in more than one registry we use the first registry in our ordered list of registries.
  • +
  • If the issuer is not in a registry we use:
  • +
      +
    • Logo: issuer.image.id otherwise issuer.image
    • +
    • Name: issuer.name
    • +
    • URL: issuer.url
    • +
    +
  • If no name, url, or image is provided in either a registry or in the VC itself then we show nothing for the missing values.
  • +
+ ) +} + +const DetailsSection = () => { + return ( +
    +
  • Typically whoever signed the Verifiable Credential, for example, a university attesting to their degrees.
  • +
  • The issuer is described in the issuer property of the Verifiable Credential.
  • +
  • The issuer may also be described in an 'issuer registry' that is completely separate from the credential.
  • +
  • The DCC LCW and VerifierPlus use issuer details from the DCC registry in preference to the details in the VC.
  • +
  • An issuer's DID MUST be listed in a trusted registry in order for VCs issued with that DID to be trusted.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
+ We always show issuer details from the registry (when available) rather than from the credential + itself as a safeguard against the possibility that someone used the signing key to issue + credentials in which they've claimed to be a different issuer. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
The issuer of the Verifiable Credential
+ ) +} + +export const issuerDetailsHelpDescription = DescriptionSection() + +export const issuerDetailsHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Valid From Date', content: DeterminationSection() }, + { sectionTitle: 'Example VC - issuer.image.id', content: ExampleImageIdSection() }, + { sectionTitle: 'Example VC - issuer.image', content: ExampleImageSection() }, + { sectionTitle: 'Example VC - No Issuer Image', content: ExampleNoImageSection() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] + + diff --git a/app/components/Help/KnownIssuerHelp/KnownIssuerHelp.js b/app/components/Help/KnownIssuerHelp/KnownIssuerHelp.js new file mode 100644 index 00000000..57e5769d --- /dev/null +++ b/app/components/Help/KnownIssuerHelp/KnownIssuerHelp.js @@ -0,0 +1,78 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const DidKeyVCSection = () => { + return +} + +const DidWebVCSection = () => { + return +} + +const DeterminationSection = () => { + return ( +
+
    +
  • VerifierPlus looks up the signing DID in the DCC registries.
  • +
  • The signing DID is listed in the proof.verificationMethod property of the Verifiable Credential
  • +
+
See the example sections for examples of a proof signed by both a did:key and a did:web.
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • Verifies that we recognize the Decentralized Identifier (DID) that signed the credential.
  • +
  • The DCC makes no claims about the authenticity or relevance of credentials signed by DIDs in its registries - only that we know about the DIDs in some way.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
+ Fundmentally important is that to trust anything signed by an issuer, + the DID (Decentralized Identififer) used to + sign the credential must be known to us in some way. That might be because + we keep a list of DIDs in our verifier that we know about (an internal registry), or it might that we look up + the DID in some kind of shared registry that we in turn trust. Both are effectively lists + of DIDs that we trust. If a credential has not been signed by a DID that we know about + then we cannot trust the credential - it could have been faked and signed by anyone. +
+ +
+ We look up DIDs in a registry controlled by the Digital Credentials Consortium. + We don't, however, make any guarantees about the trustworthiness or legitimacy of the credentials - only that they + were signed by a key that has been registered as a DID in one of our registries. We make no guarantees because our registries are + strictly for demonstration purposes. +
+ +
+ If we can't find an issuer's DID in one of our registries we only provide a warning, rather than declare the + credential completely invalid, because VerifierPlus is educational and we expect that people + will use our verification for demonstration and testing.

+ A 'real' verifier would use a registry whose registered issuers had been vetted and approved to issue specific credentials. + A registry of DIDs controlled by the association of university registrars for a given coountry, for example, + could be used to verify digital degrees from accredited universities. In this case, it might then be more accurate + to say it was a registry of 'trusted' issuers, rather than simply 'known' issuers.
+ + ) +} + +const DescriptionSection = () => { + return ( +
Checks that the credential was signed by an issuer in our registry.
+ ) +} + +export const knownIssuerFormatHelpDescription = DescriptionSection() + +export const knownIssuerHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine Who Issued the Credential', content: DeterminationSection() }, + { sectionTitle: 'Example did:key VC', content: DidKeyVCSection() }, + { sectionTitle: 'Example did:web VC', content: DidWebVCSection() }, + { sectionTitle: 'Notes', content: NotesSection() } +] diff --git a/app/components/Help/LcwRequestHelp/LcwRequestHelp.tsx b/app/components/Help/LcwRequestHelp/LcwRequestHelp.tsx new file mode 100644 index 00000000..9a66b0ae --- /dev/null +++ b/app/components/Help/LcwRequestHelp/LcwRequestHelp.tsx @@ -0,0 +1,87 @@ +import styles from '../Help.module.css'; + +const HowItWorksSection = () => { + return ( +
+
The steps in the exchange, loosely:
+
    +
  1. Click the 'Request credentials from LCW +' button in VerifierPlus (V+)
  2. +
  3. The V+ UI generates a uuid for a new exchange and encodes it into a deeplink that is shown to the user (as a QR), like this one (UUID shortened for readability): + + {`dccrequest://request?request={"credentialRequestOrigin":"https://verifierplus.org","protocols":{"vcapi":"https://verifierplus.org/api/exchanges/80c8e5f0-93b9a9"}}`} + +
  4. +
  5. The important bit in there is the exchanges endpoint, i.e, the value of the 'vcapi' property: +https://verifierplus.org/api/exchanges/80c8e5f0-93b9a9
  6. +
  7. The V+ UI now starts polling that exchange endpoint until the GET returns a result. The endpoint won't return a result until the LCW has posted a credential.
  8. +
  9. User opens the deeplink on their phone (by scanning QR with phone camera)
  10. +
  11. The LCW posts an empty object to the exchanges endpoint.
  12. +
  13. The V+ server API receives the post and returns a verifiablePresentationRequest like so: +
    {`{
    +    "verifiablePresentationRequest": {
    +      "query": [
    +        {
    +          "type": "QueryByExample",
    +          "credentialQuery": {
    +            "reason": "Please present your 
    +            Verifiable Credential to complete 
    +            the verification process.",
    +            "example": {
    +              "type": ["VerifiableCredential"]
    +            }
    +          }
    +        }
    +      ]
    +    }
    +  }
    +    `}
    +  
  14. +
  15. The LCW receives that request and posts whatever credentials the user selects to the same exchanges endpoint.
  16. +
  17. The V+ exchanges endpoint receives the posted credentials and stores them.
  18. +
  19. Now the next time the V+ UI polls the exchanges endpoint, it will get back the credentials.
  20. +
  21. Finally, the LCW verifies the credentials.
  22. +
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • Demonstrates requesting a credential from the Learner Credential Wallet (LCW).
  • +
  • Uses the Verifiable Presentation Request specification.
  • +
  • Primarily here to demonstrate how applications can make requests to wallets like the LCW.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
We've included this feature in VerifierPlus as a working demonstration + of how an application might request credentials from a wallet like the Learner Credential Wallet. In this demonstration, + once VerifierPlus has gotten the credential, it verifies it, but other applications might request a credential for other reasons, for example, to + obtain structured and verified data, say when applying for a job. +
+
This request only works + with the DCC Learner Credential Wallet because we've used a deeplink that specifically opens the LCW app. Two other APIs that + allow the end user to select which wallet they'd like are Credential Handler API (CHAPI) and the Digital Credentials API. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
Requests a Verifiable Credential from the Learner Credential Wallet (LCW).
+ ) +} + +export const LcwRequestHelpDescription = DescriptionSection() + +export const LcwRequestHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How It Works', content: HowItWorksSection() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] + + diff --git a/app/components/Help/NotVCHelp/NotVCHelp.js b/app/components/Help/NotVCHelp/NotVCHelp.js new file mode 100644 index 00000000..9aa80b2b --- /dev/null +++ b/app/components/Help/NotVCHelp/NotVCHelp.js @@ -0,0 +1,62 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const ExampleV2Section = () => { + return +} + +const ExampleV1Section = () => { + return + +} + +const DetailsSection = () => { + return ( +
    +
  • You must provide a valid Verifiable Credential in JSON format (see the example sections below).
  • +
  • If you've pasted anything other than the JSON for a Verifiable Credential or a URL (link) that returns a Verifiable Credential, you'll get this error.
  • +
  • If you've provided a URL, it might not return a Verifiable Credential, or might not be a valid URL.
  • +
  • If you've not put anything at all into the text box, you'll also get this error.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
VerifierPlus only verifies Verifiable Credentials, which + includes Open Badges version 3, but not Open Badges version 2. +
+
Be careful when copying and pasting your VC. If anything + changes in a VC after it is signed, including things like adding extra spaces, then it won't verify. Sometimes the copy/paste can + change things like quotation marks or spacing. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
Whatever you've provided as a Verifiable Credential wasn't understood by VerifierPlus.
+ ) +} + +export const notVCHelpDescription = DescriptionSection() + +export const notVCHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'Example - Verifiable Credential v1', content: ExampleV1Section() }, + { sectionTitle: 'Example - Verifiable Credential v2', content: ExampleV2Section() }, + { sectionTitle: 'Notes', content: NotesSection() } +] + + + + + + + + + + + \ No newline at end of file diff --git a/app/components/Help/NotVerifiedMessageHelp/NotVerifiedMessageHelp.tsx b/app/components/Help/NotVerifiedMessageHelp/NotVerifiedMessageHelp.tsx new file mode 100644 index 00000000..f780930e --- /dev/null +++ b/app/components/Help/NotVerifiedMessageHelp/NotVerifiedMessageHelp.tsx @@ -0,0 +1,46 @@ +import styles from '../Help.module.css'; +export const NotVerifiedMessageHelp = () => { + return <> +
    +
  • One or more of the checks during verification failed, and were severe enough that we outright reject the credential.
  • +
+
+If a credential has been tampered with, that is to say some part of the content of the credential has been changed since the +credential was signed, then we consider the entire credential invalid because we can't detect specifically what changed, only +that something changed, which means anything could have changed.

+The change may have been unintentional or because someone didn't realize that they couldn't, for example, correct a mis-spelling +in their name, but for all we know, the entire name might have been changed, or the name of the credential may have been changed (e.g., +changing a certificate of completion to a doctoral degree) - anything. So we are forced to declare the whole credential invalid. +
+
+The cyrptographic keys used to sign Verifiable Credentials are typically encoded in something called a DID (Decentralized Identifier) that +makes it easier to associate different keys with the same identifier, thereby allowing for 'key rotation' and other kinds of key management. +A fundamental aspect of DIDs therefore is that the keys asssociated with that DID are listed in a 'DID Document' and often that DID document +must be retrieved from a URL. If, however, the DID document can't be retrieved, then we can't confirm that the specific key +used to sign a credential does in fact belong to that DID. Which means the signing key could be fake. And so we must declare +the credential unverifiable. +
+
+A revoked credential is considered entirely invalid because it is possible that it never should +have been issued in the first place, in other words it never should have been considered valid. An example might be a +credential that was mistakenly issued to the wrong person.

+There are cases where a credential might be revoked even though it was valid at some point, like say if someone legally changes their name +and a new credential is subsequently issued, and the issuer doesn't want the old credential to be used any longer. The issuer could instead opt to leave the old credential valid because it does still in some sense describe +a fact that was true at the time of issuance, but that is a policy decision the issuer must make.

Even if that old credential +is revoked, it might still be useful to prove say that the holder had a different name at the time of issuance, but the verifying software +can't know that was why it was revoked. +
+
+You might reasonably ask why a reason isn't associated with a revocation. We don't associate a reason because that +reason would have to be posted publicly (so it could be retrieved by the verifier) and we typically don't want to +publicly discolose reasons for revocation, which could badly jeopardize the holder's privacy. +
+
What produces a warning
+
    +
  • The credential has been revoked.
  • +
  • The credential has been tampered with.
  • +
  • We couldn't retrieve the DID document for the DID that signed the credential.
  • +
+
+ +} \ No newline at end of file diff --git a/app/components/Help/PasteJsonUrlHelp/PasteJsonUrlHelp.js b/app/components/Help/PasteJsonUrlHelp/PasteJsonUrlHelp.js new file mode 100644 index 00000000..7353f449 --- /dev/null +++ b/app/components/Help/PasteJsonUrlHelp/PasteJsonUrlHelp.js @@ -0,0 +1,55 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const ExampleV2Section = () => { + return +} + +const ExampleV1Section = () => { + return + +} + +const DetailsSection = () => { + return ( +
    +
  • You can either paste in the credential itself, which is a text file containing the credential encoded as JSON, or provide a URL that links to the file.
  • +
  • JSON (Javascript Object Notation) is a simple way to structure data in a parent/child hierachy.
  • +
  • Examples of Verifiable Credentials are provided below in the examples sections.
  • +
  • You can alternatively upload the file directly from your computer using the text area just below labelled 'Drag and drop a file here or browse'.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
VerifierPlus only verifies Verifiable Credentials, which + includes Open Badges version 3, but not Open Badges version 2. +
+
Be careful when copying and pasting your VC. If anything + changes in a VC after it is signed, including things like adding extra spaces, then it won't verify. Sometimes the copy/paste can + change things like quotation marks or spacing. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
Paste in your Verifiable Credential or a link to it.
+ ) +} + +export const pasteJSONHelpDescription = DescriptionSection() + +export const pasteJSONHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'Example - Verifiable Credential v1', content: ExampleV1Section() }, + { sectionTitle: 'Example - Verifiable Credential v2', content: ExampleV2Section() }, + { sectionTitle: 'Notes', content: NotesSection() } +] + + + + diff --git a/app/components/Help/RegistryHelp/RegistryHelp.js b/app/components/Help/RegistryHelp/RegistryHelp.js new file mode 100644 index 00000000..00ad0d4c --- /dev/null +++ b/app/components/Help/RegistryHelp/RegistryHelp.js @@ -0,0 +1,36 @@ +import styles from '../Help.module.css'; +export const RegistryHelp = () => { + return <> +
    +
  • Verifies that we recognize the key that signed the credential.
  • +
  • VerifierPlus looks up the key in the DCC registries.
  • +
  • The DCC makes no claims about the authenticity or relevance of credentials signed by keys in its registries - only that we know about the keys in some way.
  • +
+ +

+ It is fundamentally necessary that we are told in some + trustworthy way that a public key does belong to the issuer. Otherwise + fake key pairs could be used to sign fake credentials. + An issuer can tell us directly which keys are theirs, which can be as a simple as + saying, "Hey there, my DID is did:key:z6MkjoriXdbyWD25YXTed114F8hdJrLXQ567xxPHAUKxpKkS" + or they might post it on their known + web site (e.g. mit.edu) so we can check it as needed, or they might add it to a registry + so that it can be looked up along with the + keys of other issuers. +

+

+ We look up public keys in a registry controlled by the Digital Credentials Consortium. + We don't, however, make any guarantees about the trustworthiness or legitimacy of the credentials - only that they + were signed by a key that has been registered in one of our registries. We make no guarantees because our registries are + strictly for demonstration purposes. +

+

+ A 'real' verifier would use a registry whose registered issuers had been vetted and approved to issue specific credentials. + A registry of keys controlled by the association of university registrars for a given coountry, for example, + could be used to verify digital degrees from accredited universities. In this case, it might then be more accurate + to say it was a registry of 'trusted' issuers, rather than simply 'known' issuers.

+ +

A slight nuance is that Verifiable Credentials are typically signed with a Decentralized Identifier which + is simply a more durable way to describe a signing key. So we look up DIDs in our registry.

+ +} \ No newline at end of file diff --git a/app/components/Help/RegistryListHelp/RegistryListHelp.js b/app/components/Help/RegistryListHelp/RegistryListHelp.js new file mode 100644 index 00000000..e0b2c22e --- /dev/null +++ b/app/components/Help/RegistryListHelp/RegistryListHelp.js @@ -0,0 +1,38 @@ +import styles from '../Help.module.css'; +export const RegistryListHelp = () => { + return <> +
    +
  • A list of all the registries in which the DID used to sign the credential is registered.
  • +
  • For each registry we provide the issuer name and url registered for that DID.
  • +
  • The DCC makes no claims about the legality of credentials signed by issuers who are registered in its registries - only that we know about the issuer.
  • +
  • We look up the issuer's DID dynamically so it can happen that one or more of the registries might be unreachable.
  • +
  • If we can't find the DID (that signed the credential) in any registry then we show a warning
  • +
  • In a production environment, if a DID can't be found in a registry it would be untrustworthy, unless we happen to know the DID is trustworthy for some other reason.
  • + +
+ +
+ It is fundamentally necessary that we are told in some + trustworthy way that a public key does belong to the issuer. Otherwise + fake key pairs could be used to sign fake credentials. + An issuer can tell us directly which keys are theirs, which can be as a simple as + saying, "Hey there, my DID is did:key:z6MkjoriXdbyWD25YXTed114F8hdJrLXQ567xxPHAUKxpKkS" + or they might post it on their known + web site (e.g. mit.edu) so we can check it as needed, or they can add it to a registry + so that it can be looked up along with the + keys of other issuers. +
+
+ We look up public keys in a registry controlled by the Digital Credentials Consortium. + We don't, however, make any guarantees about the trustworthiness or legitimacy of the credentials - only that they + were signed by a key that has been registered in one of our registries. We make no guarantees because our registries are + strictly for demonstration purposes. +
+
+ A 'real' verifier would use a registry whose registered issuers had been vetted and approved to issue specific credentials. + A registry of keys controlled by the association of university registrars for a given coountry, for example, + could be used to verify digital degrees from accredited universities. In this case, it might then be more accurate + to say it was a registry of 'trusted' issuers, rather than simply 'known' issuers.
+ + +} \ No newline at end of file diff --git a/app/components/Help/RevocationHelp/RevocationHelp.tsx b/app/components/Help/RevocationHelp/RevocationHelp.tsx new file mode 100644 index 00000000..c68a127c --- /dev/null +++ b/app/components/Help/RevocationHelp/RevocationHelp.tsx @@ -0,0 +1,92 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const RevokedVCSection = () => { + return +} +const UnrevokedVCSection = () => { + return +} + +const DeterminationSection = () => { + return ( + +<> +
For a VC with a 'credentialStatus' entry like the following:
+
+          {`"credentialStatus": {
+    "id": "https://example.com/list/e5Wmbrj#7",
+    "type": "BitstringStatusListEntry",
+    "statusPurpose": "revocation",
+    "statusListCredential": "https://example.com/list/e5Wmbrj",
+    "statusListIndex": "7"
+  }`}
+    
+ We retrieve the status list from the 'statusListCredential' url and then check the bit at the position indicated by 'statusListIndex'. + If it is '1' the credential has been revoked. If it is '0' it hasn't. + +
If there is no 'credentialStatus' property we say the credential has not been revoked. We could also say there is no revocation status, but that might confuse some people.
+ +
If we can't retrieve the status list we provide a warning.
+ +
See the example section for an example of a credentialStatus
+ + ) +} + +const DetailsSection = () => { + return ( +
    +
  • A revoked credential is considered entirely invalid.
  • +
  • Revocation is optional - not required by Verifiable Credential data model nor the OBv3 data model.
  • +
  • Revocation applies only to the VC, not to the underlying credential.
  • +
  • The DCC uses the BitstringStatusList implementation.
  • +
  • The DCC ignores any credentialStatus whose type is not BitstringStatusListEntry.
  • +
+ ) +} + +const NotesSection = () => { + return ( + <> +
A very + important point here is that the credentialStatus field revokes the + Verifiable Credential, and NOT the underlying credential. So the VC issued for a degree might be + revoked, but that does not imply the degree itself was revoked. It might be that the degree was + in fact revoked, but the credentialStatus only applies to the VC. +
+ +
Other approaches + are possible - a simple list of credential ids for example - + but the BitstringStatusList has a good mix of pretty-good-privacy, simplicity, and especially size, for most needs.

+A bitstring + is simply a list of zeros and ones like so: 010000100. For that list, the credential that has been assigned the + second position and third last positions in the list have been revoked. If credentials have + been assigned the other positions they have not been revoked.

+ When a verifier is checking a given credential they ask the issuer for the entire bistring ( + which ideally is very long, so thousands of bits providing status for thousands of VCs), but without telling the + issuer which specific credential they are checking. This is often called 'herd-privacy'. +
+ + + ) +} + +const DescriptionSection = () => { + return ( +
Whether or not the credential has been revoked.
+ ) +} + +export const revocationHelpDescription = DescriptionSection() + +export const revocationHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine Revocation Status', content: DeterminationSection() }, + { sectionTitle: 'Example VC - revoked status', content: RevokedVCSection() }, + { sectionTitle: 'Example VC - unrevoked status', content: UnrevokedVCSection() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] + + + diff --git a/app/components/Help/ScanQRHelp/ScanQRHelp.js b/app/components/Help/ScanQRHelp/ScanQRHelp.js new file mode 100644 index 00000000..4ee1c1d1 --- /dev/null +++ b/app/components/Help/ScanQRHelp/ScanQRHelp.js @@ -0,0 +1,46 @@ +import styles from '../Help.module.css'; + +const DetailsSection = () => { + return ( +
    +
  • Using your phone or computer camera you can scan a QR that either: +
      +
    • links to a Verifiable Credential
    • +
    • links to a Verifiable Presentation that wraps a Verifiable Credential
    • +
    • encodes a Verifiable Presentation (containing a Verifiable Credential) directly into the QR as CBOR-LD
    • +
  • +
  • If a link, VerifierPlus fetches the Verifiable Credential and/or extract it from the Verifiable Presentation.
  • +
  • If a CBOR-LD encoded VP, VerifierPlus extracts the Verifiable Credential.
  • +
  • VerifierPlus verifies whatever Verifiable Credential is returned or extracted.
  • +
) +} + +const ExampleSection = () => { + return ( +
+ Here is a sample QR you can scan to see how it works. Note that you'll need two devices to + do this, or will have to print the QR before scanning. +
+ +
+
+ ) +} + +const DescriptionSection = () => { + return ( +
Scans a QR code pointing to, or containing, a Verifiable Credential, and verifies it.
+ ) +} + +export const scanQRHelpDescription = DescriptionSection() + +export const scanQRtHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'Example', content: ExampleSection() } +] + + + + + diff --git a/app/components/Help/SignatureHelp/SignatureHelp.tsx b/app/components/Help/SignatureHelp/SignatureHelp.tsx new file mode 100644 index 00000000..455fc450 --- /dev/null +++ b/app/components/Help/SignatureHelp/SignatureHelp.tsx @@ -0,0 +1,56 @@ +import styles from '../Help.module.css'; + +const DeterminationSection = () => { + return ( + <> +
How we determine the validity of the signature:
+
    +
  • The credential must not have been tampered with.
  • +
  • We must be able to retrieve the DID document for the DID that signed the credential, and that DID document must include the public key used to sign the credential.
  • +
+ ) +} + +const DetailsSection = () => { + return ( +
    +
  • Verifies that the cryptographic signature in the credential matches the content, in other words, verifies that the credential has not been tampered with.
  • +
  • Fails if the public signing key cannot be retrieved from the DID document of the DID used to sign the credential.
+ ) +} + +const NotesSection = () => { + return ( + <> +
If anything at all changes in the credential, sometimes even just adding + a blank space, the credential is no longer valid because the content no longer matches the content that was used to create + the specific cryptographic signature for the specific credential. +
+
If the signature is invalid, we cannot trust anything + in the credential - not the name of the credential, the date, the issuer - absolutely nothing. If the signature is invalid + you might ask the holder if they had changed anything in it, and if so, to give you the credential as it was + when the issuer gave them their copy. +
+
If we can't retrieve the DID document for the DID that signed the + credential, or if the public signing key that was used to sign the credential is not in the DID document, then signature verification fails + because we can't know that the public key was ever 'registered' to that DID. This isn't an issue when using the did:key DID method + which doesn't require a separate DID document, + but is required for did:web and did:webvh methods or any method that stores the DID document remotely. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
Checks that the credential has not been tampered with.
+ ) +} + +export const signatureHelpDescription = DescriptionSection() + +export const signatureHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Check the Signature', content: DeterminationSection() }, + { sectionTitle: 'Notes', content: NotesSection() } +] diff --git a/app/components/Help/TitleHelp/TitleHelp.tsx b/app/components/Help/TitleHelp/TitleHelp.tsx new file mode 100644 index 00000000..c90044a6 --- /dev/null +++ b/app/components/Help/TitleHelp/TitleHelp.tsx @@ -0,0 +1,100 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const ExampleOldDCCSection = () => { + return +} + +const ExampleOBv3Section = () => { +return + +} +const ImageDeterminationSection = () => { + return ( +
+
    +
  • credential.credentialSubject.achievement.image.id
  • +
+
If there is no value available then we show nothing.
+
See the OBv3 example section below for a working example.
+
) +} + +const NameDeterminationSection = () => { + return ( +
+
For an Open Badge version 3:
+
    +
  • credential.credentialSubject.achievement.name
  • +
+
For a legacy DCC Verifiable Credential:
+
    +
  • credential.credentialSubject.hasCredential.name
  • +
+
If no value is available for either then we show nothing.
+
See the example sections for working examples of each.
+
) +} + +const DetailsSection = () => { + return ( +
+
Credential Name
+
    +
  • The name of the credential, or in other words, the name of the achievement declared by the credential.
  • +
+
Credential Image
+
    +
  • An image for the credential, sometimes called a badge image.
  • +
  • The image value can be a url or the raw base64 encoded data for the image.
  • +
+
+ ) +} + +const NameNotesSection = () => { + return ( + <> +
Some issuers might make the name + unique to the holder. So where a a generic name might be Bachelor of Arts, some issuers might set the name as + Charlie Jones' Bachelor of Arts. +
+ + ) +} + + +const ImageNotesSection = () => { + return ( + <> +
In VerifierPlus we size the + image to 36px by 36px regardless of the actual size of the image. +
+
Generally the same image is used for all instances of the credential, so not unique to the recipient, but they can be unique. It's up to the issuer. +
+
A base64 encoded image contains the entire image, which is usually large, and so makes the credential itself very large. A url, on the other hand, points at the image, and doesn't affect the size of the credential. +
+
The image returned from a URL can be changed after issuance. A base64 encoded image can't.
+
If the url for the image stops working, the image can't be displayed. A base64 image will always continue to work.
+ + + ) +} + +const DescriptionSection = () => { + return ( +
The name and image for the credential.
+ ) +} + +export const titleHelpDescription = DescriptionSection() + +export const titleHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Name', content: NameDeterminationSection() }, + { sectionTitle: 'How We Determine the Image', content: ImageDeterminationSection() }, + { sectionTitle: 'Example - Open Badge verison 3', content: ExampleOBv3Section() }, + { sectionTitle: 'Example - legacy DCC VC', content: ExampleOldDCCSection() }, + { sectionTitle: 'Image Notes', content: ImageNotesSection( )}, + { sectionTitle: 'Name Notes', content: NameNotesSection( )} +] diff --git a/app/components/Help/VCHelp.js b/app/components/Help/VCHelp.js new file mode 100644 index 00000000..dfea9444 --- /dev/null +++ b/app/components/Help/VCHelp.js @@ -0,0 +1,5 @@ +export const VCHelp = () => { + return <> +

A Verifiable Credential is a cryptographically signed digital credential.

+ +} \ No newline at end of file diff --git a/app/components/Help/ValidFromHelp/ValidFromHelp.tsx b/app/components/Help/ValidFromHelp/ValidFromHelp.tsx new file mode 100644 index 00000000..78a92ead --- /dev/null +++ b/app/components/Help/ValidFromHelp/ValidFromHelp.tsx @@ -0,0 +1,67 @@ +import styles from '../Help.module.css'; +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; + +const ExampleV2Section = () => { + return +} + +const ExampleV1Section = () => { +return + +} +const DeterminationSection = () => { + return ( +
+
For Version 1 of the VC data model:
+
    +
  • issuanceDate (required)
  • +
+
For Version 2 of the VC data model, in order of preference:
+
    +
  • validFrom (optional)
  • +
  • proof.created (required)
  • +
+
See the example sections for working examples of each.
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • The date at which the attestation of the Verifiable Credential became valid.
  • +
  • The date is set directly in the Verifiable Credential and cannot be changed without invalidating the cryptographic signature.
  • +
  • The underlying credential (e.g., a degree) may have a different issued/awarded date.
  • +
  • OpenBadges v3 records the date a degree was awarded using awardedDate.
  • + +
+ ) +} + +const NotesSection = () => { + return ( + <> +
The Valid From date is not + necessarily the date when the underlying credential became valid, but rather the date the attestation of the VC itself became valid. + So, for example, a degree might have been issued to a graduate in 1994, but a VC attesting to the degree, might only have become + valid in 2025. In other words the attestation and the thing being attested to (the claim) are two different + things. +
+ + ) +} + +const DescriptionSection = () => { + return ( +
The date from which the Verifiable Credential is considered valid.
+ ) +} + +export const validFromHelpDescription = DescriptionSection() + +export const validFromHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Valid From Date', content: DeterminationSection() }, + { sectionTitle: 'Example - Verifiable Credential v1', content: ExampleV1Section() }, + { sectionTitle: 'Example - Verifiable Credential v2', content: ExampleV2Section() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] diff --git a/app/components/Help/ValidUntilHelp/ValidUntilHelp.tsx b/app/components/Help/ValidUntilHelp/ValidUntilHelp.tsx new file mode 100644 index 00000000..504b94a8 --- /dev/null +++ b/app/components/Help/ValidUntilHelp/ValidUntilHelp.tsx @@ -0,0 +1,57 @@ +import { VcDisplay } from '@/components/VcDisplay/VcDisplay'; +import styles from '../Help.module.css'; + +const ExampleV2Section = () => { + return +} + +const ExampleV1Section = () => { +return + +} +const DeterminationSection = () => { + return ( +
+
According to the version of the Verifiable Credentials data model:
+
    +
  • Version 1: expirationDate
  • +
  • Version 2: validUntil
  • +
+
See the example sections for working examples of each.
+
) +} + +const DetailsSection = () => { + return ( +
    +
  • The validUntil date is set directly in the Verifiable Credential and cannot be changed without invalidating the cryptographic signature.
  • +
  • The validUntil date is not required. A credential can be issued that never expires.
  • +
  • The validUntil date applies to the Verifiable Credential. The underlying credential attested to by the Verifiable Credential may have a different expiry date, or none at all.
  • +
+ ) +} + +const NotesSection = () => { + return ( +
Sometimes a credential is still useful even though it has expired. An expired driver's licence, for + example, can still be used to prove our age. Or to prove that we were authorized to drive during a given period, + which might be useful when applying for car insurance. +
+ ) +} + +const DescriptionSection = () => { + return ( +
The date until which the credential is considered valid.
+ ) +} + +export const validUntilHelpDescription = DescriptionSection() + +export const validUntilHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Valid Until Date', content: DeterminationSection() }, + { sectionTitle: 'Example - Verifiable Credential v1', content: ExampleV1Section() }, + { sectionTitle: 'Example - Verifiable Credential v2', content: ExampleV2Section() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] \ No newline at end of file diff --git a/app/components/Help/VerificationStatusHelp/VerificationStatusHelp.tsx b/app/components/Help/VerificationStatusHelp/VerificationStatusHelp.tsx new file mode 100644 index 00000000..9563d648 --- /dev/null +++ b/app/components/Help/VerificationStatusHelp/VerificationStatusHelp.tsx @@ -0,0 +1,56 @@ +import styles from '../Help.module.css'; + +const DeterminationSection = () => { + return ( +
    +
  • Verifying if verification is still in progress.
  • +
  • Verified if nothing has been tampered with, the credential hasn't expired, hasn't been revoked, and is from a known issuer.
  • +
  • Not Verified if the credential has been tampered with, the signature can't been checked, or the credential has been revoked.
  • +
  • Warning if the credential hasn't been tampered with, but has expired, or isn't from a known issuer.
  • +
+) +} + +const DetailsSection = () => { + return ( +
    + Four possible statuses: Verified, Not Verified , Warning or Verifying +
+ ) +} + +const NotesSection = () => { + return ( + <> +
We only flag a credential as Not Verified is if: +

+
    +
  • the credential has been tampered with
  • +
  • we can't otherwise verify the signature
  • +
  • the credential has been revoked
  • +

+
If, however, the credential + has expired or is from an issuer we don't recognize we take a moderate approach + and simply show a Warning.
+
+
We are less strict because our verification page is primarily for education and + demonstration. A verification page for a university that verifies degrees would likely more strictly enforce + expiry, and would most certainly reject credentials from an unknown issuer.
+
+ + ) +} + +const DescriptionSection = () => { + return ( +
An indication of the status of the credential.
+ ) +} + +export const verificationStatusHelpDescription = DescriptionSection() + +export const verificationStatusHelpSections = [ + { sectionTitle: 'Details', content: DetailsSection() }, + { sectionTitle: 'How We Determine the Status', content: DeterminationSection() }, + { sectionTitle: 'Notes', content: NotesSection( )} +] diff --git a/app/components/Help/WarningMessageHelp/WarningMessageHelp.tsx b/app/components/Help/WarningMessageHelp/WarningMessageHelp.tsx new file mode 100644 index 00000000..be6969d3 --- /dev/null +++ b/app/components/Help/WarningMessageHelp/WarningMessageHelp.tsx @@ -0,0 +1,32 @@ +import styles from '../Help.module.css'; +export const WarningMessageHelp = () => { + return <> +
    +
  • A warning that something about the credential either couldn't be conclusively checked or there was a failing check despite which the credential might still be useful.
  • +
  • It is very important to note that this site is for education and demonstration, and so we can be less strict with our checks.
  • +
  • Verification sites for 'production' credentials would be more strict.
  • +
+
+ If the credential + has expired or is from an issuer we don't recognize then we take a moderate approach + and simply show the warning message. We do this because our verification page is primarily for education and + demonstration. A verification page for a university that verifies degrees would likely more strictly enforce + expiry, and would most certainly reject credentials from an unknown issuer. +
+
+ It might happen during verification that a registry in which the signing DID is listed can't be reached, maybe + because of a server outage, or poor connection. This is another reason why we don't outright reject credentials whose + signing DID can't be found in a registry. Again, a production site would likely show a much stronger message because + fundamentally, if we don't recognize the signing DID, then we have no idea who it belongs to, and could just + as easily be fake as not. +
+
What produces a warning
+
    +
  • The credential has expired.
  • +
  • The credential was signed by an unrecognized DID (Decentralized Identifier), in other words, we couldn't find it in a registry.
  • +
+
+ + + +} \ No newline at end of file diff --git a/app/components/Help/index.js b/app/components/Help/index.js new file mode 100644 index 00000000..f9bef351 --- /dev/null +++ b/app/components/Help/index.js @@ -0,0 +1,24 @@ +export * from './IssuerDetailsHelp/IssuerDetailsHelp'; +export * from './KnownIssuerHelp/KnownIssuerHelp'; +export * from './ValidUntilHelp/ValidUntilHelp'; +export * from './ValidFromHelp/ValidFromHelp'; +export * from './RegistryHelp/RegistryHelp'; +export * from './HolderHelp/HolderHelp' +export * from './DescriptionHelp/DescriptionHelp' +export * from './CriteriaHelp/CriteriaHelp' +export * from './VerificationStatusHelp/VerificationStatusHelp' +export * from './TitleHelp/TitleHelp' +export * from './AchievementTypeHelp/AchievementTypeHelp' +export * from './CredentialFormatHelp/CredentialFormatHelp' +export * from './SignatureHelp/SignatureHelp' +export * from './RevocationHelp/RevocationHelp' +export * from './RegistryListHelp/RegistryListHelp' +export * from './LcwRequestHelp/LcwRequestHelp' +export * from './PasteJsonUrlHelp/PasteJsonUrlHelp' +export * from './DragAndDropHelp/DragAndDropHelp' +export * from './ScanQRHelp/ScanQRHelp' +export * from './ChapiHelp/ChapiHelp' +export * from './WarningMessageHelp/WarningMessageHelp' +export * from './NotVerifiedMessageHelp/NotVerifiedMessageHelp' +export * from './NotVCHelp/NotVCHelp' +export * from './AlignmentHelp/AlignmentHelp' \ No newline at end of file diff --git a/app/components/InfoBlock/InfoBlock.d.ts b/app/components/InfoBlock/InfoBlock.d.ts index 8a7a3633..b58d6dc7 100644 --- a/app/components/InfoBlock/InfoBlock.d.ts +++ b/app/components/InfoBlock/InfoBlock.d.ts @@ -1,5 +1,11 @@ +import { CollapsibleSectionProps } from "../ContextualHelp/ContextualHelp"; + export type InfoBlockProps = { header: string; contents: string; testId?: string; + HelpContent?: ReactElement; + helpTitle?:string; + helpDescription?: ReactNode; + helpSections?: CollapsibleSectionProps[] } \ No newline at end of file diff --git a/app/components/InfoBlock/InfoBlock.tsx b/app/components/InfoBlock/InfoBlock.tsx index 6ab06d01..9460b94a 100644 --- a/app/components/InfoBlock/InfoBlock.tsx +++ b/app/components/InfoBlock/InfoBlock.tsx @@ -1,11 +1,13 @@ import type { InfoBlockProps } from './InfoBlock.d'; import styles from './InfoBlock.module.css'; -export const InfoBlock = ({header, contents, testId}: InfoBlockProps) => { +import { ContextualHelp } from '@/components/ContextualHelp/ContextualHelp' + +export const InfoBlock = ({header, contents, testId, helpTitle, HelpContent, helpSections, helpDescription}: InfoBlockProps) => { // The testId allows playwright to more reliably find this element on the page return (
-

{header}

+

{header}{(HelpContent||helpSections)&&{HelpContent && }}

{contents}
); diff --git a/app/components/Issuer/Issuer.tsx b/app/components/Issuer/Issuer.tsx index 7266db66..2026cc43 100644 --- a/app/components/Issuer/Issuer.tsx +++ b/app/components/Issuer/Issuer.tsx @@ -2,6 +2,8 @@ import { useRef } from 'react'; import type { IssuerProps } from './Issuer.d'; import styles from './Issuer.module.css'; import { TestId } from '@/lib/testIds'; +import { ContextualHelp } from '@/components/ContextualHelp/ContextualHelp' +import { issuerDetailsHelpDescription, issuerDetailsHelpSections } from '@/components/Help'; export const Issuer = ({ issuer, header, infoButtonPushed }: IssuerProps) => { const issuerImage = useRef(null); @@ -16,7 +18,7 @@ export const Issuer = ({ issuer, header, infoButtonPushed }: IssuerProps) => {
{(issuer?.image || issuer?.name || issuer?.url) && (
-

{header}

+

{header}

{issuer.image && ( {`${issuer.name} @@ -29,6 +31,7 @@ export const Issuer = ({ issuer, header, infoButtonPushed }: IssuerProps) => {

{issuer.address}

{issuer.url} +
diff --git a/app/components/RegistryCard/RegistryCard.module.css b/app/components/RegistryCard/RegistryCard.module.css index 55ad78fe..39113813 100644 --- a/app/components/RegistryCard/RegistryCard.module.css +++ b/app/components/RegistryCard/RegistryCard.module.css @@ -27,8 +27,8 @@ } .logoPlaceholder img { - width: 40px; - height: 40px; + max-width: 200px; + max-height: 28px; border: 1px solid #ccc; border-radius: 4px; background-color: #f9f9f9; diff --git a/app/components/RegistryCard/RegistryCard.tsx b/app/components/RegistryCard/RegistryCard.tsx index ba3cc355..6053dbd2 100644 --- a/app/components/RegistryCard/RegistryCard.tsx +++ b/app/components/RegistryCard/RegistryCard.tsx @@ -1,4 +1,3 @@ - import React from 'react'; import styles from './RegistryCard.module.css'; import { TestId } from '@/lib/testIds'; @@ -23,9 +22,6 @@ export const RegistryCard: React.FC = ({ registryName, issuer (More info on governance) ) : null} - - -
{issuerLogo && (
diff --git a/app/components/ResultLog/ResultLog.d.ts b/app/components/ResultLog/ResultLog.d.ts index eba91e39..31d27c8b 100644 --- a/app/components/ResultLog/ResultLog.d.ts +++ b/app/components/ResultLog/ResultLog.d.ts @@ -1,5 +1,20 @@ import { VerifyResponse } from "types/credential"; +import { CollapsibleSectionProps } from "../ContextualHelp/ContextualHelp"; export type ResultLogProps = { verificationResult: VerifyResponse; +} + +export type ResultItem = { + verified:boolean, + positiveMessage:string, + negativeMessage?:string, + warningMessage?:string, + sourceLogId?:string, + testId:string, + helpTitle?:string, + HelpContent?:ReactElement, + helpSections?:CollapsibleSectionProps[] + helpDescription?:ReactElement, + issuer?:boolean } \ No newline at end of file diff --git a/app/components/ResultLog/ResultLog.tsx b/app/components/ResultLog/ResultLog.tsx index 933ab704..6a9a5df9 100644 --- a/app/components/ResultLog/ResultLog.tsx +++ b/app/components/ResultLog/ResultLog.tsx @@ -1,10 +1,12 @@ 'use client' import { useState } from 'react'; import { CredentialError } from '@/types/credential.d'; -import type { ResultLogProps } from './ResultLog.d'; +import type { ResultItem, ResultLogProps } from './ResultLog.d'; import styles from './ResultLog.module.css'; import { StatusPurpose, hasStatusPurpose } from '@/lib/credentialStatus'; import { TestId } from "@/lib/testIds" +import { revocationHelpDescription, revocationHelpSections, credentialFormatHelpDescription, credentialFormatHelpSections, validUntilHelpSections, signatureHelpDescription, signatureHelpSections, knownIssuerFormatHelpDescription, knownIssuerHelpSections, validUntilHelpDescription } from '../Help'; +import { ContextualHelp } from '../ContextualHelp/ContextualHelp'; export enum LogId { ValidSignature = 'valid_signature', @@ -20,7 +22,7 @@ export enum LogMessages { HasNotExpired = 'has not expired', GeneralError = 'There was an error verifing this credential.', UnknownError = 'There was an unknown error verifing this credential.', - WellFormed = 'is in a supported credential format', + WellFormed = 'is correctly formatted', MalFormed = 'is not a recognized credential type', ValidSignature = 'has a valid signature', InvalidSignature = 'has an invalid signature', @@ -44,8 +46,12 @@ export const ResultLog = ({ verificationResult }: ResultLogProps) => { warningMessage = '', sourceLogId = '', testId = '', + helpTitle, + HelpContent, + helpSections, + helpDescription, issuer = false - }) => { + }: ResultItem) => { const isIssuerCheck = sourceLogId === LogId.IssuerDIDResolves; const isExpirationCheck = sourceLogId === LogId.Expiration; const status = verified @@ -61,7 +67,7 @@ export const ResultLog = ({ verificationResult }: ResultLogProps) => { }; return ( -
+
{ ? 'priority_high' : 'close'} -
+
{status === 'positive' && positiveMessage} {status === 'warning' && warningMessage} {status === 'negative' && negativeMessage} + {(helpSections)&&
}
); @@ -183,6 +190,9 @@ export const ResultLog = ({ verificationResult }: ResultLogProps) => { positiveMessage={LogMessages.WellFormed} negativeMessage={LogMessages.MalFormed} testId={TestId.MalformedLogMsg} + helpDescription={credentialFormatHelpDescription} + helpSections={credentialFormatHelpSections} + helpTitle="Supported Credential Format" /> { positiveMessage={LogMessages.ValidSignature} negativeMessage={LogMessages.InvalidSignature} testId={TestId.SigningLogMsg} + helpDescription={signatureHelpDescription} + helpSections={signatureHelpSections} + helpTitle="Valid Signature" /> { sourceLogId={LogId.IssuerDIDResolves} testId={TestId.IssuerLogMsg} issuer={true} + helpSections={knownIssuerHelpSections} + helpDescription={knownIssuerFormatHelpDescription} + helpTitle="Known Issuer" /> { @@ -206,6 +222,9 @@ export const ResultLog = ({ verificationResult }: ResultLogProps) => { positiveMessage={LogMessages.NotRevoked} negativeMessage={verificationResult.hasStatusError ? LogMessages.UncheckedRevocation : LogMessages.Revoked} testId={TestId.RevocationLogMsg} + helpDescription={revocationHelpDescription} + helpSections={revocationHelpSections} + helpTitle="Revocation" /> } {/*
*/} @@ -218,6 +237,9 @@ export const ResultLog = ({ verificationResult }: ResultLogProps) => { warningMessage={LogMessages.HasExpired} sourceLogId={LogId.Expiration} testId={TestId.ExpirationLogMsg} + helpDescription={validUntilHelpDescription} + helpSections={validUntilHelpSections} + helpTitle="Expiration Date" /> {hasCredentialStatus && hasSuspensionStatus && diff --git a/app/components/ToggleSwitch/ToggleSwitch.d.ts b/app/components/ToggleSwitch/ToggleSwitch.d.ts index 33ba4339..5a49d82a 100644 --- a/app/components/ToggleSwitch/ToggleSwitch.d.ts +++ b/app/components/ToggleSwitch/ToggleSwitch.d.ts @@ -4,4 +4,6 @@ export type ToggleSwitchProps = { icon: ReactElement; isOn: boolean; handleToggle: () => void; + name: string; + ariaLabel: string; } \ No newline at end of file diff --git a/app/components/ToggleSwitch/ToggleSwitch.tsx b/app/components/ToggleSwitch/ToggleSwitch.tsx index 24718182..c9da351f 100644 --- a/app/components/ToggleSwitch/ToggleSwitch.tsx +++ b/app/components/ToggleSwitch/ToggleSwitch.tsx @@ -1,20 +1,20 @@ import type {ToggleSwitchProps} from './ToggleSwitch.d' import styles from './ToggleSwitch.module.css'; -export const ToggleSwitch = ({ icon, isOn, handleToggle }: ToggleSwitchProps) => { +export const ToggleSwitch = ({ icon, isOn, handleToggle, name, ariaLabel }: ToggleSwitchProps) => { return(