Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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