Skip to content

Commit

Permalink
Add verifiable credentials support in frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
nmattia committed Dec 4, 2023
1 parent a140c40 commit e249395
Show file tree
Hide file tree
Showing 17 changed files with 1,142 additions and 2 deletions.
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
"watch:showcase": "astro check --root ./src/showcase --watch",
"watch": "npm run check -- --watch",
"opts": "NODE_OPTIONS='--loader ts-node/esm --experimental-specifier-resolution=node' \"$@\"",
"generate": "npm run generate:types && npm run generate:js",
"generate": "npm run generate:types && npm run generate:js && npm run generate:types-issuer && npm run generate:js-issuer",
"generate:types": "didc bind ./src/internet_identity/internet_identity.did -t ts > src/frontend/generated/internet_identity_types.d.ts",
"generate:js": "didc bind ./src/internet_identity/internet_identity.did -t js > src/frontend/generated/internet_identity_idl.js",
"generate:types-issuer": "didc bind ./demos/vc_issuer/vc_issuer.did -t ts > src/frontend/generated/vc_issuer_types.d.ts",
"generate:js-issuer": "didc bind ./demos/vc_issuer/vc_issuer.did -t js > src/frontend/generated/vc_issuer_idl.js",
"build:showcase": "tsc --noEmit && astro check --root ./src/showcase && astro build --root ./src/showcase",
"preview:showcase": "astro preview --root ./src/showcase",
"screenshots": "npm run opts -- ./src/frontend/screenshots.ts",
Expand Down Expand Up @@ -62,6 +64,7 @@
"bip39": "^3.0.4",
"buffer": "^6.0.3",
"idb-keyval": "^6.2.1",
"jose": "^5.1.3",
"lit-html": "^2.7.2",
"process": "^0.11.10",
"qr-creator": "^1.0.0",
Expand Down
103 changes: 103 additions & 0 deletions src/frontend/generated/vc_issuer_idl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
export const idlFactory = ({ IDL }) => {
const IssuerConfig = IDL.Record({
'idp_canister_ids' : IDL.Vec(IDL.Principal),
'ic_root_key_der' : IDL.Vec(IDL.Nat8),
});
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 GetCredentialResponse = IDL.Variant({
'Ok' : IssuedCredentialData,
'Err' : IssueCredentialError,
});
const HeaderField = IDL.Tuple(IDL.Text, IDL.Text);
const HttpRequest = IDL.Record({
'url' : IDL.Text,
'method' : IDL.Text,
'body' : IDL.Vec(IDL.Nat8),
'headers' : IDL.Vec(HeaderField),
});
const HttpResponse = IDL.Record({
'body' : IDL.Vec(IDL.Nat8),
'headers' : IDL.Vec(HeaderField),
'status_code' : IDL.Nat16,
});
const PrepareCredentialRequest = IDL.Record({
'signed_id_alias' : SignedIdAlias,
'credential_spec' : CredentialSpec,
});
const PreparedCredentialData = IDL.Record({
'prepared_context' : IDL.Opt(IDL.Vec(IDL.Nat8)),
});
const PrepareCredentialResponse = 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,
'error_code' : IDL.Nat64,
});
const Icrc21Error = IDL.Variant({
'GenericError' : Icrc21ErrorInfo,
'UnsupportedCanisterCall' : Icrc21ErrorInfo,
'ConsentMessageUnavailable' : Icrc21ErrorInfo,
});
const Icrc21ConsentMessageResponse = IDL.Variant({
'Ok' : Icrc21ConsentInfo,
'Err' : Icrc21Error,
});
return IDL.Service({
'add_employee' : IDL.Func([IDL.Principal], [IDL.Text], []),
'add_graduate' : IDL.Func([IDL.Principal], [IDL.Text], []),
'configure' : IDL.Func([IssuerConfig], [], []),
'get_credential' : IDL.Func(
[GetCredentialRequest],
[GetCredentialResponse],
['query'],
),
'http_request' : IDL.Func([HttpRequest], [HttpResponse], ['query']),
'prepare_credential' : IDL.Func(
[PrepareCredentialRequest],
[PrepareCredentialResponse],
[],
),
'vc_consent_message' : IDL.Func(
[Icrc21VcConsentMessageRequest],
[Icrc21ConsentMessageResponse],
[],
),
});
};
export const init = ({ IDL }) => {
const IssuerConfig = IDL.Record({
'idp_canister_ids' : IDL.Vec(IDL.Principal),
'ic_root_key_der' : IDL.Vec(IDL.Nat8),
});
return [IDL.Opt(IssuerConfig)];
};
84 changes: 84 additions & 0 deletions src/frontend/generated/vc_issuer_types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { Principal } from '@dfinity/principal';
import type { ActorMethod } from '@dfinity/agent';
import type { IDL } from '@dfinity/candid';

export type ArgumentValue = { 'Int' : number } |
{ 'String' : string };
export interface CredentialSpec {
'arguments' : [] | [Array<[string, ArgumentValue]>],
'credential_type' : string,
}
export interface GetCredentialRequest {
'signed_id_alias' : SignedIdAlias,
'prepared_context' : [] | [Uint8Array | number[]],
'credential_spec' : CredentialSpec,
}
export type GetCredentialResponse = { 'Ok' : IssuedCredentialData } |
{ 'Err' : IssueCredentialError };
export type HeaderField = [string, string];
export interface HttpRequest {
'url' : string,
'method' : string,
'body' : Uint8Array | number[],
'headers' : Array<HeaderField>,
}
export interface HttpResponse {
'body' : Uint8Array | number[],
'headers' : Array<HeaderField>,
'status_code' : number,
}
export interface Icrc21ConsentInfo {
'consent_message' : string,
'language' : string,
}
export type Icrc21ConsentMessageResponse = { 'Ok' : Icrc21ConsentInfo } |
{ 'Err' : Icrc21Error };
export interface Icrc21ConsentPreferences { 'language' : string }
export type Icrc21Error = { 'GenericError' : Icrc21ErrorInfo } |
{ 'UnsupportedCanisterCall' : Icrc21ErrorInfo } |
{ 'ConsentMessageUnavailable' : Icrc21ErrorInfo };
export interface Icrc21ErrorInfo {
'description' : string,
'error_code' : bigint,
}
export interface Icrc21VcConsentMessageRequest {
'preferences' : Icrc21ConsentPreferences,
'credential_spec' : CredentialSpec,
}
export type IssueCredentialError = { 'Internal' : string } |
{ 'SignatureNotFound' : string } |
{ 'InvalidIdAlias' : string } |
{ 'UnauthorizedSubject' : string } |
{ 'UnknownSubject' : string } |
{ 'UnsupportedCredentialSpec' : string };
export interface IssuedCredentialData { 'vc_jws' : string }
export interface IssuerConfig {
'idp_canister_ids' : Array<Principal>,
'ic_root_key_der' : Uint8Array | number[],
}
export interface PrepareCredentialRequest {
'signed_id_alias' : SignedIdAlias,
'credential_spec' : CredentialSpec,
}
export type PrepareCredentialResponse = { 'Ok' : PreparedCredentialData } |
{ 'Err' : IssueCredentialError };
export interface PreparedCredentialData {
'prepared_context' : [] | [Uint8Array | number[]],
}
export interface SignedIdAlias { 'credential_jws' : string }
export interface _SERVICE {
'add_employee' : ActorMethod<[Principal], string>,
'add_graduate' : ActorMethod<[Principal], string>,
'configure' : ActorMethod<[IssuerConfig], undefined>,
'get_credential' : ActorMethod<[GetCredentialRequest], GetCredentialResponse>,
'http_request' : ActorMethod<[HttpRequest], HttpResponse>,
'prepare_credential' : ActorMethod<
[PrepareCredentialRequest],
PrepareCredentialResponse
>,
'vc_consent_message' : ActorMethod<
[Icrc21VcConsentMessageRequest],
Icrc21ConsentMessageResponse
>,
}
export declare const idlFactory: IDL.InterfaceFactory;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"en": {
"title": "Credential Sharing Aborted",
"ok": "Ok",
"aborted_internal_error": "There was an unexpected issue while verifying your credentials. Please retry, and if the problem persist contact support.",
"aborted_auth_failed_ii": "There was an error authenticating you with your Internet Identity. Aliases were not created.",
"aborted_auth_failed_issuer": "There was an error authenticating you with the issuer. Make sure you can authenticate with the issuer.",
"aborted_issuer_api_error": "There was an error requesting credentials with the issuer. The issuer returned an error.",
"aborted_bad_principal_rp": "The principal provided by the relying party does not match the data Internet Identity has.",
"aborted_no_canister_id": "Internet Identity could not find the canister for the issuer.",
"notice": "We will now let the relying party know there was an issue.",
"you_may_close": "You may now close this page."
}
}
84 changes: 84 additions & 0 deletions src/frontend/src/flows/verifiableCredentials/abortedCredentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { mainWindow } from "$src/components/mainWindow";
import { toast } from "$src/components/toast";
import { I18n } from "$src/i18n";
import { mount, renderPage } from "$src/utils/lit-html";
import { Chan } from "$src/utils/utils";
import { TemplateResult, html } from "lit-html";
import { asyncReplace } from "lit-html/directives/async-replace.js";

import copyJson from "./abortedCredentials.json";

export type AbortReason =
| "internal_error"
| "auth_failed_ii"
| "auth_failed_issuer"
| "issuer_api_error"
| "bad_principal_rp"
| "no_canister_id";

/* A screen telling the user the flow was aborted and giving information
* on why it was aborted and what they can do about it. */
const abortedCredentialsTemplate = ({
i18n,
reason,
onAcknowledge,
scrollToTop = false,
}: {
i18n: I18n;
reason: AbortReason;
onAcknowledge: () => void;
/* put the page into view */
scrollToTop?: boolean;
}): TemplateResult => {
const copy = i18n.i18n(copyJson);

const didAck = new Chan(false);
const ack = () => {
didAck.send(true);
toast.info(html`${copy.you_may_close}`);
return onAcknowledge();
};

const slot = html`
<hgroup
data-page="vc-allow"
${scrollToTop ? mount(() => window.scrollTo(0, 0)) : undefined}
>
<h1 class="t-title t-title--main">${copy.title}</h1>
</hgroup>
<p class="t-paragraph">${copy[`aborted_${reason}`]}</p>
<p class="t-paragraph">${copy.notice}</p>
<button
data-action="cancel"
?disabled=${asyncReplace(didAck)}
class="l-stack c-button c-button--primary"
@click="${() => ack()}"
>
${copy.ok}
</button>
`;

return mainWindow({
showFooter: false,
showLogo: false,
slot,
});
};

export const abortedCredentialsPage = renderPage(abortedCredentialsTemplate);
export const abortedCredentials = ({
reason,
}: {
reason: AbortReason;
}): Promise<"aborted"> => {
return new Promise((resolve) =>
abortedCredentialsPage({
i18n: new I18n(),
reason,
onAcknowledge: () => resolve("aborted"),
scrollToTop: true,
})
);
};
Loading

0 comments on commit e249395

Please sign in to comment.