import React, { useState } from "react"; import { Flex, Button, Field, Flash, Form, Input, Loader, Checkbox, Text } from "rimble-ui"; import { Warning } from "@rimble/icons"; import { mutate } from "swr"; import { JsonSchemaNode, VcSchema, VC } from "vc-schema-tools"; import { ModalBack, ModalContent, ModalContentFullWidth, ModalHeader } from "../../../elements/Modals"; import { Identifier } from "../../../../types"; import { DidSelect } from "../../../elements/DidSelect"; import { IssueVcFormInput } from "./IssueVcFormInput"; import { SertoUiContext, SertoUiContextInterface } from "../../../../context/SertoUiContext"; import { SchemaDataInput } from "../../Schemas"; import { IssueVcSuccess } from "./IssueVcSuccess"; import { Credential } from "../Credential"; import { H4 } from "../../../layouts/LayoutComponents"; import { colors } from "../../../../themes"; import { getLdTypesFromSchemaResponse, getSchemaUris } from "../../Schemas/utils"; const STEPS = ["FORM", "REVIEW", "ISSUED"]; function buildCredential( schema: SchemaDataInput, initialCred: Partial, issuer: string, vcData: { [key: string]: any }, ): Partial { const { subjectLdType, credLdType } = getLdTypesFromSchemaResponse(schema); const uris = getSchemaUris(schema); let ldContext = uris.jsonLdContext; if (!ldContext) { console.warn("Could not find JSON-LD context URL - embedding entire context in VC"); ldContext = JSON.parse(schema.ldContext || "{}")?.["@context"]; } const jsonSchemaUrl = uris.jsonSchema; if (!jsonSchemaUrl) { console.warn("Could not find JSON Schema URL - excluding `credentialSchema` property from VC"); } return { ...initialCred, "@context": ldContext ? ["https://www.w3.org/2018/credentials/v1", ldContext] : "https://www.w3.org/2018/credentials/v1", type: credLdType ? [...(initialCred.type || []), credLdType] : initialCred.type, issuer: { id: issuer, }, credentialSchema: jsonSchemaUrl ? { id: jsonSchemaUrl, type: "JsonSchemaValidator2018", } : undefined, credentialSubject: { ...(subjectLdType ? { type: subjectLdType } : {}), ...vcData, }, }; } export interface IssueVcFormProps { schema: SchemaDataInput | null; identifiers?: Identifier[]; subjectIdentifier?: Identifier; onComplete(): void; onVcDataChange?(vcData: any): void; goBack?(): void; } export const IssueVcForm: React.FunctionComponent = (props) => { const { schema, subjectIdentifier, onComplete, onVcDataChange } = props; const context = React.useContext(SertoUiContext); let identifiers = props.identifiers || context.userDids; if (!identifiers?.[0]) { console.error( "Could not get user DIDs: no `identifiers` prop supplied and no `userDids` property has been supplied to SertoUiContext via SertoUiProvider", ); identifiers = []; } const initialCred = { "@context": ["https://www.w3.org/2018/credentials/v1"], // "id": "uuid:9110652b-3676-4720-8139-9163b244680d", // @TODO Should the API generate this? type: ["VerifiableCredential"], issuer: { id: identifiers[0]?.did }, issuanceDate: new Date().toISOString(), credentialSubject: { id: subjectIdentifier?.did || identifiers[0]?.did, exampleData: { foo: 123, bar: true, }, }, }; const [step, setStep] = React.useState(STEPS[0]); const [loading, setLoading] = React.useState(false); const [vcToIssue, setVcToIssue] = useState(); const [issuedVc, setIssuedVc] = useState(); const [issueAndSend, setIssueAndSend] = React.useState(false); const [subjectSupportsMessaging, setSubjectSupportsMessaging] = React.useState(false); const [error, setError] = useState(); const [messagingError, setMessagingError] = useState(); const [vcData, setVcData] = useState<{ [key: string]: any }>({}); const [rawJsonVc, setRawJsonVc] = useState(JSON.stringify(initialCred, null, 2)); const [issuer, setIssuer] = useState(identifiers[0]?.did); const schemaInstance = React.useMemo(() => { if (schema) { try { setError(""); return new VcSchema(schema.jsonSchema); } catch (err) { console.error("Failed to generate schema instance:", err); setError(err.toString()); } } }, [schema]); const credSchema = schemaInstance?.jsonSchema?.properties?.credentialSubject; React.useEffect(() => { onVcDataChange?.(vcData); }, [onVcDataChange, vcData]); if (!context.issueVc) { console.error( "issueVc function not present in SertoUiContext. Make sure to supply this function to the context passed in to SertoUiProvider or else serto-ui library cannot issue a VC for you.", ); return ( Missing issue VC functionality ); } function createVc(e: Event) { e.preventDefault(); setError(undefined); try { let credential: any; if (schema) { if (!schemaInstance) { console.error("Can't issue VC: schema instance is undefined. Schema data:", schema); setError("Could not initialize schema instance"); return; } credential = buildCredential(schema, initialCred, issuer, vcData); } else { credential = JSON.parse(rawJsonVc); } // @TODO/tobek Actually validate VC according to schema instance onVcDataChange?.(credential); setVcToIssue(credential); setStep(STEPS[1]); } catch (err) { console.error("failed to create VC:", err); setError("Failed to create VC: " + err.message); return; } } async function issueVc(e: Event) { e.preventDefault(); setError(undefined); setLoading(true); try { const issueResponse = await context.issueVc?.(vcToIssue, { revocable: false, keepCopy: true, save: "true", proofFormat: "jwt", }); console.log("issued VC, response:", issueResponse); mutate("/v1/agent/dataStoreORMGetVerifiableCredentials"); setIssuedVc(issueResponse); if (issueAndSend) { const subject = issueResponse.credentialSubject?.id; if (!subject) { console.error("failed to send VC: VC has no credentialSubject.id", issueResponse); setMessagingError( 'Sending credential to subject failed: credential does not contain "credentialSubject.id" property to send to.', ); } else if (!context.sendVc) { console.error("serto-ui context missing sendVc", context); setMessagingError("This Serto instance is missing the ability to send VCs via DIDComm."); } else { try { const sendResponse = await context.sendVc(issuer, subject, issueResponse); console.log("sent VC, response:", sendResponse); // We're done - no need to show final share step. context.toastProvider.addMessage("Credential issued & sent", { colorTheme: "light", variant: "success", }); onComplete(); return; } catch (err) { console.error("failed to send VC:", err); setMessagingError("Sending credential to subject failed: " + err.message); } } } context.toastProvider.addMessage("Credential issued", { colorTheme: "light", variant: "success", }); setLoading(false); setStep(STEPS[2]); } catch (err) { console.error("failed to issue VC:", err); setError("Failed to issue VC: " + err.message); setLoading(false); return; } } const showBackButton = !!props.goBack || step !== STEPS[0]; function goBack() { if (step === STEPS[0] && props.goBack) { props.goBack(); } else if (step !== STEPS[0]) { setStep(STEPS[STEPS.indexOf(step) - 1]); } } if (step === STEPS[2] && issuedVc) { return ( ); } if (step === STEPS[1] && vcToIssue) { return ( <> {showBackButton && } Issue {schema?.name?.replace(/\s?Credential$/, "")} Credential {schema && ( <>

Recipient Information

{!subjectSupportsMessaging && ( {vcToIssue.credentialSubject?.id ? ( <> The credential subject DID you selected ( {vcToIssue.credentialSubject.id}) does not support receiving credentials. ) : ( "Your credential doesn't have a subject DID to automatically send the issued credential to." )}{" "} You may send it via link, QR code, or email after issuing the credential. )} setIssueAndSend(!issueAndSend)} disabled={!subjectSupportsMessaging} /> )} {/* If there's no schema, then there's no VC wizard form, just the generic VC JSON textarea, so there's no DidSearch component checking for messaging support. We could in theory fire off a check for messaging support here, but not worth building that right now: the JSON input is a power user/edge case feature, and they can still go ahead to the post-issue step and send via DIDComm or whatever from there. */} {error && ( {error} )}
); } return ( <> {showBackButton && } Issue {schema?.name?.replace(/\s?Credential$/, "")} Credential
{!credSchema?.properties ? ( {/*@NOTE Rimble Textarea component doesn't work - uses instead of