Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion frontends/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"ol-test-utilities": "0.0.0"
},
"dependencies": {
"@mitodl/mitxonline-api-axios": "^2025.11.24",
"@mitodl/mitxonline-api-axios": "^2025.12.18",
"@tanstack/react-query": "^5.66.0",
"axios": "^1.12.2",
"tiny-invariant": "^1.3.3"
Expand Down
111 changes: 111 additions & 0 deletions frontends/api/src/test-utils/factories/mitxonline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,63 @@ import type {
} from "@mitodl/mitxonline-api-axios/v2"
import { PartialFactory } from "ol-test-utilities"

/**
Copy link
Contributor

Choose a reason for hiding this comment

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

I find it odd that there isn't a standard maintained Typescript library we can import for this, but it seems like what most people do is just implement their own type like this.

* VerifiableCredential type matching the structure defined in DigitalCredentialDialog.
* This interface defines the structure of a VerifiableCredential (Open Badges v3.0).
*/
export interface VerifiableCredential {
id: string
type: string[]
proof?: {
type: string
created: string
proofValue: string
cryptosuite: string
proofPurpose: string
verificationMethod: string
}
issuer: {
id: string
name: string
type: string[]
image?: {
id: string
type: string
caption?: string
}
}
"@context": string[]
validFrom: string
validUntil: string
credentialSubject: {
type: string[]
identifier: Array<{
salt: string
type: string
hashed: boolean
identityHash: string
identityType: string
}>
achievement: {
id: string
name: string
type: string[]
image?: {
id: string
type: string
caption?: string
}
criteria?: {
narrative: string
}
description: string
achievementType: string
}
activityEndDate?: string
activityStartDate?: string
}
}

export const courseCertificate: PartialFactory<V2CourseRunCertificate> = (
overrides = {},
) => {
Expand Down Expand Up @@ -55,6 +112,7 @@ export const courseCertificate: PartialFactory<V2CourseRunCertificate> = (
},
],
},
verifiable_credential_json: verifiableCredential(),
}

return { ...base, ...overrides } as V2CourseRunCertificate
Expand Down Expand Up @@ -104,7 +162,60 @@ export const programCertificate: PartialFactory<V2ProgramCertificate> = (
},
],
},
verifiable_credential_json: verifiableCredential(),
}

return { ...base, ...overrides } as V2ProgramCertificate
}

export const verifiableCredential: PartialFactory<VerifiableCredential> = (
overrides = {},
) => {
const base: VerifiableCredential = {
id: `https://example.com/credentials/${faker.string.uuid()}`,
type: ["VerifiableCredential", "OpenBadgeCredential"],
proof: {
type: "DataIntegrityProof",
created: faker.date.past().toISOString(),
proofValue: `z${faker.string.alphanumeric(16)}`,
cryptosuite: "eddsa-rdfc-2022",
proofPurpose: "assertionMethod",
verificationMethod: `https://example.com/keys/${faker.number.int()}`,
},
issuer: {
id: "https://example.com/issuer",
name: "MIT Open Learning",
type: ["Profile"],
},
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
],
validFrom: faker.date.past().toISOString(),
validUntil: faker.date.future().toISOString(),
credentialSubject: {
type: ["AchievementSubject"],
identifier: [
{
salt: faker.string.alphanumeric(6),
type: "IdentityHash",
hashed: true,
identityHash: faker.person.fullName(),
identityType: "email",
},
],
achievement: {
id: `https://example.com/achievements/${faker.number.int()}`,
name: faker.lorem.words(3),
type: ["Achievement"],
description: faker.lorem.sentence(),
achievementType: "Certificate",
criteria: {
narrative: faker.lorem.sentence(),
},
},
},
}

return { ...base, ...overrides } as VerifiableCredential
}
3 changes: 3 additions & 0 deletions frontends/main/public/images/icons/verify.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions frontends/main/src/app-pages/CertificatePage/CertificatePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import type {
SignatoryItem,
} from "@mitodl/mitxonline-api-axios/v2"
import SharePopover from "./SharePopover"
import {
DigitalCredentialDialog,
type VerifiableCredential,
} from "./DigitalCredentialDialog"

const Page = styled.div(({ theme }) => ({
backgroundImage: `url(${backgroundImage.src})`,
Expand Down Expand Up @@ -662,6 +666,9 @@ const CertificatePage: React.FC<{
uuid: string
pageUrl: string
}> = ({ certificateType, uuid, pageUrl }) => {
const [digitalCredentialDialogOpen, setDigitalCredentialDialogOpen] =
useState(false)

const {
data: courseCertificateData,
isLoading: isCourseLoading,
Expand Down Expand Up @@ -750,6 +757,11 @@ const CertificatePage: React.FC<{
? "Module Certificate"
: `${programCertificateData?.program.program_type} Certificate`

const verifiableCredential =
certificateType === CertificateType.Course
? courseCertificateData?.verifiable_credential_json
: programCertificateData?.verifiable_credential_json

return (
<Page>
<SharePopover
Expand All @@ -759,6 +771,11 @@ const CertificatePage: React.FC<{
onClose={() => setShareOpen(false)}
pageUrl={pageUrl}
/>
<DigitalCredentialDialog
verifiableCredential={verifiableCredential as VerifiableCredential}
open={digitalCredentialDialogOpen}
onClose={() => setDigitalCredentialDialogOpen(false)}
/>
<Title>
<Typography variant="h3">
<strong>{title}</strong> {displayType}
Expand All @@ -772,6 +789,13 @@ const CertificatePage: React.FC<{
>
Download PDF
</Button>
<Button
variant="bordered"
startIcon={<RiDownloadLine />}
onClick={() => setDigitalCredentialDialogOpen(true)}
>
Download Digital Credential
</Button>
<Button
variant="bordered"
startIcon={<RiShareLine />}
Expand Down
Loading
Loading