Skip to content

Commit

Permalink
feat: add course completion logic to the issuer
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonath-z committed Nov 11, 2024
1 parent 3f9230d commit caae4c5
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 56 deletions.
1 change: 1 addition & 0 deletions .well-known/ii-alternative-origins
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://dacade.org
2 changes: 1 addition & 1 deletion src/components/sections/profile/communities/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default function SubmissionList(): ReactElement {
<SubmissionCard
key={submission.id}
stats
link={navigation.community.submissionPath(submission.id, submission.challenge.id, community?.slug)}
link={navigation.community.submissionPath(submission.id, submission.challenge?.id, community?.slug)}
submission={submission}
last={i === submissions.length - 1}
/>
Expand Down
10 changes: 8 additions & 2 deletions src/hooks/useIcpAuth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { canisterId, createActor } from "@/utilities/icp/issuerFactory";
import { Identity } from "@dfinity/agent";
import { AuthClient } from "@dfinity/auth-client";
import { Principal } from "@dfinity/principal";
Expand All @@ -14,7 +15,7 @@ type Auth = {
declare global {
interface Window {
auth: Auth;
canister: any;
issuerCanister: any;
}
}

Expand All @@ -30,12 +31,17 @@ const useIcpAuth = () => {
async function initializeContract() {
const authClient = await AuthClient.create();
window.auth = {} as any;
window.canister = {};
window.auth.client = authClient;
window.auth.isAuthenticated = await authClient.isAuthenticated();
window.auth.identity = authClient.getIdentity();
window.auth.principal = authClient.getIdentity()?.getPrincipal();
window.auth.principalText = authClient.getIdentity()?.getPrincipal().toText();
window.issuerCanister = createActor(canisterId ?? "", {
agentOptions: {
host: "https://icp0.io",
identity: authClient.getIdentity(),
},
});
}

initializeContract();
Expand Down
65 changes: 12 additions & 53 deletions src/pages/achievements/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@ import MintCertificate from "@/components/sections/profile/modals/MintCertificat
import { Certificate } from "@/types/certificate";
import { User } from "@/types/bounty";
import { IRootState } from "@/store";
import useIcpAuth, { IDENTITY_PROVIDER } from "@/hooks/useIcpAuth";
import { requestVerifiablePresentation, type VerifiablePresentationResponse } from "@dfinity/verifiable-credentials/request-verifiable-presentation";
import { Principal } from "@dfinity/principal";
import useIcpAuth from "@/hooks/useIcpAuth";
import axiosInstance from "@/config/axios";

/**
* interface for Achievement multiSelector
Expand All @@ -46,7 +45,6 @@ const Achievement = () => {
user: (state: IRootState) => state.user.data,
});
const [showMintCertificate, setShowMintCertificate] = useState(false);
const [jwtVC, setJwtVc] = useState("");
const dispatch = useDispatch();
const { locale, query } = useRouter();
const { login } = useIcpAuth();
Expand Down Expand Up @@ -113,45 +111,20 @@ const Achievement = () => {
return !achievement?.metadata?.image?.includes("/img/certificates/");
}, [achievement]);

const requestVC = (principal: string) => {
const addCourseCompletionToTheIssuerCanister = async () => {
if (!achievement?.metadata) return;
const { issuedOn, linkToWork, issuerName, name, narrative, image, comment } = achievement?.metadata;
const { name } = achievement?.metadata;
if (window.issuerCanister) return;
await window.issuerCanister.add_course_completion(name.toLowerCase().replaceAll(" ", "-"));

requestVerifiablePresentation({
onSuccess: (verifiablePresentation: VerifiablePresentationResponse) => {
const verifiablePresentationData = (verifiablePresentation as { Ok: string })?.Ok;
if (verifiablePresentationData) {
setJwtVc(verifiablePresentationData);
}
},
onError: (err: any) => {
console.log("An error occurred", err);
},
issuerData: {
origin: "https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/",
canisterId: Principal.fromText("bu5ax-5iaaa-aaaam-qbgcq-cai"),
},
credentialData: {
credentialSpec: {
credentialType: `${name} completion`,
arguments: {
issuedOn,
linkToWork,
image,
narrative,
comment,
issuerName,
name,
},
},
credentialSubject: Principal.fromText(principal),
},
identityProvider: new URL(IDENTITY_PROVIDER),
await axiosInstance.post("/certificate/complete", {
certificateId: achievement.id,
});
await findCertificateById();
};

const onMint = () => {
if (isICPSubmission) return login((principal) => requestVC(principal));
if (isICPSubmission) return login(() => addCourseCompletionToTheIssuerCanister());
setShowMintCertificate(true);
};

Expand Down Expand Up @@ -216,9 +189,9 @@ const Achievement = () => {
Mint certificate
</ArrowButton>
)}
{belongsToCurrentUser && !jwtVC && (
{belongsToCurrentUser && !achievement.completed && (
<ArrowButton target="__blank" variant="primary" className="flex ml-auto mt-5" onClick={onMint}>
Request verifiable credential
Record Course Completion
</ArrowButton>
)}
</div>
Expand All @@ -243,20 +216,6 @@ const Achievement = () => {
</AchievementViewItem>
</div>
)}
{achievement.community.name === "Internet Computer" && !!jwtVC && (
<div className="flex flex-col gap-1">
<AchievementViewItem name={"Credential JWT presentation"}>
<small
onClick={() => {
window.navigator.clipboard.writeText(jwtVC);
}}
className="font-normal line-clamp-1 text-start my-2 cursor-pointer"
>
{jwtVC}
</small>
</AchievementViewItem>
</div>
)}
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/types/certificate.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface Certificate {
submission: Submission;
minting: Minting;
user: User;
completed: boolean;
}

/**
Expand Down
99 changes: 99 additions & 0 deletions src/utilities/icp/issuer.did.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
*
* This file is an auto generated file and act as ABI for the ICP issuer canister
* DO NOT MODIFY instead refer to the latest issuer version https://github.com/dacadeorg/dacade-vc-issuer-rust
*/

export const idlFactory = ({ IDL }: any) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const SettingsInput = IDL.Record({
ii_canister_id: IDL.Principal,
ic_root_key_der: IDL.Vec(IDL.Nat8),
});
const Result = IDL.Variant({ Ok: IDL.Text, Err: IDL.Text });
const DerivationOriginRequest = IDL.Record({
frontend_hostname: IDL.Text,
});
const DerivationOriginData = IDL.Record({ origin: IDL.Text });
const DerivationOriginError = IDL.Variant({
Internal: IDL.Text,
UnsupportedOrigin: IDL.Text,
});
const Result_1 = IDL.Variant({
Ok: DerivationOriginData,
Err: DerivationOriginError,
});
const SignedIdAlias = IDL.Record({ credential_jws: IDL.Text });
const ArgumentValue = IDL.Variant({ Int: IDL.Int32, String: IDL.Text });
const CredentialSpec = IDL.Record({
arguments: IDL.Opt(IDL.Vec(IDL.Tuple(IDL.Text, ArgumentValue))),
credential_type: IDL.Text,
});
const GetCredentialRequest = IDL.Record({
signed_id_alias: SignedIdAlias,
prepared_context: IDL.Opt(IDL.Vec(IDL.Nat8)),
credential_spec: CredentialSpec,
});
const IssuedCredentialData = IDL.Record({ vc_jws: IDL.Text });
const IssueCredentialError = IDL.Variant({
Internal: IDL.Text,
SignatureNotFound: IDL.Text,
InvalidIdAlias: IDL.Text,
UnauthorizedSubject: IDL.Text,
UnknownSubject: IDL.Text,
UnsupportedCredentialSpec: IDL.Text,
});
const Result_2 = IDL.Variant({
Ok: IssuedCredentialData,
Err: IssueCredentialError,
});
const PrepareCredentialRequest = IDL.Record({
signed_id_alias: SignedIdAlias,
credential_spec: CredentialSpec,
});
const PreparedCredentialData = IDL.Record({
prepared_context: IDL.Opt(IDL.Vec(IDL.Nat8)),
});
const Result_3 = IDL.Variant({
Ok: PreparedCredentialData,
Err: IssueCredentialError,
});
const Icrc21ConsentPreferences = IDL.Record({ language: IDL.Text });
const Icrc21VcConsentMessageRequest = IDL.Record({
preferences: Icrc21ConsentPreferences,
credential_spec: CredentialSpec,
});
const Icrc21ConsentInfo = IDL.Record({
consent_message: IDL.Text,
language: IDL.Text,
});
const Icrc21ErrorInfo = IDL.Record({ description: IDL.Text });
const Icrc21Error = IDL.Variant({
GenericError: IDL.Record({
description: IDL.Text,
error_code: IDL.Nat,
}),
UnsupportedCanisterCall: Icrc21ErrorInfo,
ConsentMessageUnavailable: Icrc21ErrorInfo,
});
const Result_4 = IDL.Variant({
Ok: Icrc21ConsentInfo,
Err: Icrc21Error,
});
return IDL.Service({
add_course_completion: IDL.Func([IDL.Text], [Result], []),
derivation_origin: IDL.Func([DerivationOriginRequest], [Result_1], []),
get_credential: IDL.Func([GetCredentialRequest], [Result_2], ["query"]),
get_ii_id: IDL.Func([], [IDL.Text], []),
has_completed_course: IDL.Func([IDL.Text, IDL.Principal], [IDL.Bool], []),
prepare_credential: IDL.Func([PrepareCredentialRequest], [Result_3], []),
vc_consent_message: IDL.Func([Icrc21VcConsentMessageRequest], [Result_4], []),
});
};
export const init = ({ IDL }: any) => {
const SettingsInput = IDL.Record({
ii_canister_id: IDL.Principal,
ic_root_key_der: IDL.Vec(IDL.Nat8),
});
return [SettingsInput];
};
31 changes: 31 additions & 0 deletions src/utilities/icp/issuerFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Actor, HttpAgent, HttpAgentOptions } from "@dfinity/agent";

import { idlFactory } from "./issuer.did";

export const canisterId = process.env.NEXT_PUBLIC_ISSUER_CANISTER_ID;

type ActorOptions = {
agent?: any;
agentOptions?: HttpAgentOptions | any;
actorOptions?: any;
};

export const createActor = (canisterId: string, options: ActorOptions = {}) => {
const agent = options.agent || new HttpAgent({ ...options.agentOptions });

// Creates an actor with using the candid interface and the HttpAgent
return Actor.createActor(idlFactory, {
agent,
canisterId,
...(options ? options.actorOptions : {}),
});
};

/**
* A ready-to-use agent for the issuer canister
* @type {import("@dfinity/agent").ActorSubclass<import("./issuer.did.js")._SERVICE>}
*/

if (!canisterId) {
throw new Error("Canister id is not found");
}

0 comments on commit caae4c5

Please sign in to comment.