diff --git a/js/apps/account-ui/src/account-security/AccountRow.tsx b/js/apps/account-ui/src/account-security/AccountRow.tsx index 06ce9396a0..37b539a07b 100644 --- a/js/apps/account-ui/src/account-security/AccountRow.tsx +++ b/js/apps/account-ui/src/account-security/AccountRow.tsx @@ -12,7 +12,11 @@ import { } from "@patternfly/react-core"; import { LinkIcon, UnlinkIcon } from "@patternfly/react-icons"; import { useTranslation } from "react-i18next"; -import { IconMapper, useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; +import { + IconMapper, + useAlerts, + useEnvironment, +} from "@keycloak/keycloak-ui-shared"; import { linkAccount, unLinkAccount } from "../api/methods"; import { LinkedAccountRepresentation } from "../api/representations"; diff --git a/js/apps/account-ui/src/account-security/DeviceActivity.tsx b/js/apps/account-ui/src/account-security/DeviceActivity.tsx index 23e03253ca..061f788166 100644 --- a/js/apps/account-ui/src/account-security/DeviceActivity.tsx +++ b/js/apps/account-ui/src/account-security/DeviceActivity.tsx @@ -23,7 +23,11 @@ import { } from "@patternfly/react-icons"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { ContinueCancelModal, useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; +import { + ContinueCancelModal, + useAlerts, + useEnvironment, +} from "@keycloak/keycloak-ui-shared"; import { deleteSession, getDevices } from "../api/methods"; import { ClientRepresentation, diff --git a/js/apps/account-ui/src/api.ts b/js/apps/account-ui/src/api.ts index d897bdad86..d5a8f58fda 100644 --- a/js/apps/account-ui/src/api.ts +++ b/js/apps/account-ui/src/api.ts @@ -3,7 +3,13 @@ import { BaseEnvironment } from "@keycloak/keycloak-ui-shared/dist/context/envir import { CallOptions } from "./api/methods"; import { Links, parseLinks } from "./api/parse-links"; import { parseResponse } from "./api/parse-response"; -import { Permission, Resource, Scope, CredentialsIssuer, SupportedCredentialConfiguration } from "./api/representations"; +import { + Permission, + Resource, + Scope, + CredentialsIssuer, + SupportedCredentialConfiguration, +} from "./api/representations"; import { request } from "./api/request"; import { joinPath } from "./utils/joinPath"; @@ -70,16 +76,20 @@ function checkResponse(response: T) { return response; } - -export async function getIssuer( - context: KeycloakContext -) { +export async function getIssuer(context: KeycloakContext) { const response = await request( - "/realms/" + context.environment.realm + "/.well-known/openid-credential-issuer", + "/realms/" + + context.environment.realm + + "/.well-known/openid-credential-issuer", context, {}, new URL( - joinPath(context.environment.authUrl + "/realms/" + context.environment.realm + "/.well-known/openid-credential-issuer"), + joinPath( + context.environment.authUrl + + "/realms/" + + context.environment.realm + + "/.well-known/openid-credential-issuer", + ), ), ); return parseResponse(response); @@ -87,16 +97,26 @@ export async function getIssuer( export async function requestVCOffer( context: KeycloakContext, - supportedCredentialConfiguration:SupportedCredentialConfiguration, - credentialsIssuer:CredentialsIssuer + supportedCredentialConfiguration: SupportedCredentialConfiguration, + credentialsIssuer: CredentialsIssuer, ) { const response = await request( "/protocol/oid4vc/credential-offer-uri", context, - { searchParams: {"credential_configuration_id": supportedCredentialConfiguration.id, "type": "qr-code", "width": "500", "height": "500"} }, + { + searchParams: { + credential_configuration_id: supportedCredentialConfiguration.id, + type: "qr-code", + width: "500", + height: "500", + }, + }, new URL( - joinPath(credentialsIssuer.credential_issuer + "/protocol/oid4vc/credential-offer-uri"), + joinPath( + credentialsIssuer.credential_issuer + + "/protocol/oid4vc/credential-offer-uri", + ), ), ); - return response.blob() -} \ No newline at end of file + return response.blob(); +} diff --git a/js/apps/account-ui/src/api/representations.ts b/js/apps/account-ui/src/api/representations.ts index 291d4512e8..4c4a8c5621 100644 --- a/js/apps/account-ui/src/api/representations.ts +++ b/js/apps/account-ui/src/api/representations.ts @@ -211,6 +211,9 @@ export interface SupportedCredentialConfiguration { export interface CredentialsIssuer { credential_issuer: string; credential_endpoint: string; - authorization_servers: string[]; - credential_configurations_supported: Record -} \ No newline at end of file + authorization_servers: string[]; + credential_configurations_supported: Record< + string, + SupportedCredentialConfiguration + >; +} diff --git a/js/apps/account-ui/src/api/request.ts b/js/apps/account-ui/src/api/request.ts index fd2b0673e0..31a5aa283b 100644 --- a/js/apps/account-ui/src/api/request.ts +++ b/js/apps/account-ui/src/api/request.ts @@ -37,10 +37,10 @@ export async function request( path: string, { environment, keycloak }: KeycloakContext, opts: RequestOptions = {}, - fullUrl?: URL + fullUrl?: URL, ) { - if (typeof fullUrl === 'undefined') { - fullUrl = url(environment, path) + if (typeof fullUrl === "undefined") { + fullUrl = url(environment, path); } return _request(fullUrl, { ...opts, diff --git a/js/apps/account-ui/src/applications/Applications.tsx b/js/apps/account-ui/src/applications/Applications.tsx index 6a6e3b2156..ab30781bf8 100644 --- a/js/apps/account-ui/src/applications/Applications.tsx +++ b/js/apps/account-ui/src/applications/Applications.tsx @@ -22,7 +22,11 @@ import { } from "@patternfly/react-icons"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { ContinueCancelModal, useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; +import { + ContinueCancelModal, + useAlerts, + useEnvironment, +} from "@keycloak/keycloak-ui-shared"; import { deleteConsent, getApplications } from "../api/methods"; import { ClientRepresentation } from "../api/representations"; import { Page } from "../components/page/Page"; diff --git a/js/apps/account-ui/src/oid4vci/Oid4Vci.tsx b/js/apps/account-ui/src/oid4vci/Oid4Vci.tsx index 94c9a3a2f9..31b8488347 100644 --- a/js/apps/account-ui/src/oid4vci/Oid4Vci.tsx +++ b/js/apps/account-ui/src/oid4vci/Oid4Vci.tsx @@ -1,132 +1,136 @@ +import { useEnvironment } from "@keycloak/keycloak-ui-shared"; import { - Select, - SelectList, - SelectOption, - PageSectionVariants, - PageSection, - ActionList, - ActionListItem, - List, - ListItem, - MenuToggleElement, - MenuToggle -} from '@patternfly/react-core'; -import { useEffect, useState, useMemo } from "react"; + ActionList, + ActionListItem, + List, + ListItem, + MenuToggle, + MenuToggleElement, + PageSection, + PageSectionVariants, + Select, + SelectList, + SelectOption, +} from "@patternfly/react-core"; +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; -import { usePromise } from "../utils/usePromise"; -import { Page } from "../components/page/Page"; -import { CredentialsIssuer } from "../api/representations"; import { getIssuer, requestVCOffer } from "../api"; +import { CredentialsIssuer } from "../api/representations"; +import { Page } from "../components/page/Page"; +import { usePromise } from "../utils/usePromise"; export const Oid4Vci = () => { - const context = useEnvironment(); - - const { t } = useTranslation(); + const context = useEnvironment(); - const initialSelected = t('verifiableCredentialsSelectionDefault') + const { t } = useTranslation(); - const [selected, setSelected] = useState(initialSelected); - const [qrCode, setQrCode] = useState("") - const [isOpen, setIsOpen] = useState(false) - const [offerQRVisible, setOfferQRVisible] = useState(false) - const [credentialsIssuer, setCredentialsIssuer] = useState() + const initialSelected = t("verifiableCredentialsSelectionDefault"); - usePromise(() => getIssuer(context), setCredentialsIssuer); + const [selected, setSelected] = useState(initialSelected); + const [qrCode, setQrCode] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const [offerQRVisible, setOfferQRVisible] = useState(false); + const [credentialsIssuer, setCredentialsIssuer] = + useState(); - const selectOptions = useMemo( - () => { - if(typeof credentialsIssuer !== 'undefined') { - return credentialsIssuer.credential_configurations_supported - } - return {} - }, - [credentialsIssuer], - ) + usePromise(() => getIssuer(context), setCredentialsIssuer); - const dropdownItems = useMemo( - () => { - if (typeof selectOptions !== 'undefined') { - return Array.from(Object.keys(selectOptions)) - } - return [] - }, - [selectOptions], - ) + const selectOptions = useMemo(() => { + if (typeof credentialsIssuer !== "undefined") { + return credentialsIssuer.credential_configurations_supported; + } + return {}; + }, [credentialsIssuer]); - useEffect(() => { - if(initialSelected !== selected && credentialsIssuer !== undefined){ - requestVCOffer(context, selectOptions[selected], credentialsIssuer) - .then((blob) => { - var reader = new FileReader(); - reader.readAsDataURL(blob) - reader.onloadend = function() { - let result = reader.result - if (typeof result === "string") { - setQrCode(result); - setOfferQRVisible(true); - setIsOpen(false); - } + const dropdownItems = useMemo(() => { + if (typeof selectOptions !== "undefined") { + return Array.from(Object.keys(selectOptions)); + } + return []; + }, [selectOptions]); + + useEffect(() => { + if (initialSelected !== selected && credentialsIssuer !== undefined) { + requestVCOffer(context, selectOptions[selected], credentialsIssuer).then( + (blob) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = function () { + const result = reader.result; + if (typeof result === "string") { + setQrCode(result); + setOfferQRVisible(true); + setIsOpen(false); } - }) - } - }, [selected]); - - const onToggleClick = () => { - setIsOpen(!isOpen); - }; - - const toggle = (toggleRef: React.Ref) => ( - - {selected} - + }; + }, ); + } + }, [selected]); - return ( - - - - - setIsOpen(isOpen)} + onSelect={(_event, val) => setSelected(val as string)} + isOpen={isOpen} + selected={selected} + toggle={toggle} + shouldFocusToggleOnSelect={true} + > + + {dropdownItems.map((option) => ( + {option} - - ))} - - - - - - { offerQRVisible && - - - - } - - - - - - ); + + ))} + + + + + + {offerQRVisible && ( + + + + )} + + + + + + ); }; - -export default Oid4Vci; \ No newline at end of file +export default Oid4Vci; diff --git a/js/apps/account-ui/src/resources/ResourcesTab.tsx b/js/apps/account-ui/src/resources/ResourcesTab.tsx index 1e76646471..828ef18367 100644 --- a/js/apps/account-ui/src/resources/ResourcesTab.tsx +++ b/js/apps/account-ui/src/resources/ResourcesTab.tsx @@ -32,7 +32,11 @@ import { } from "@patternfly/react-table"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { ContinueCancelModal, useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; +import { + ContinueCancelModal, + useAlerts, + useEnvironment, +} from "@keycloak/keycloak-ui-shared"; import { fetchPermission, fetchResources, updatePermissions } from "../api"; import { getPermissionRequests } from "../api/methods"; import { Links } from "../api/parse-links"; diff --git a/js/apps/account-ui/src/routes.tsx b/js/apps/account-ui/src/routes.tsx index 23fa6fa627..3aafc25141 100644 --- a/js/apps/account-ui/src/routes.tsx +++ b/js/apps/account-ui/src/routes.tsx @@ -61,7 +61,7 @@ export const PersonalInfoRoute: IndexRouteObject = { export const Oid4VciRoute: RouteObject = { path: "oid4vci", element: , -} +}; export const RootRoute: RouteObject = { path: decodeURIComponent(new URL(environment.baseUrl).pathname), diff --git a/js/apps/account-ui/test/oid4vci/oid4vci.spec.ts b/js/apps/account-ui/test/oid4vci/oid4vci.spec.ts index dc0a0001b5..bc18043afd 100644 --- a/js/apps/account-ui/test/oid4vci/oid4vci.spec.ts +++ b/js/apps/account-ui/test/oid4vci/oid4vci.spec.ts @@ -1,19 +1,17 @@ import { expect, test } from "@playwright/test"; import { login } from "../login"; -const realm = "verifiable-credentials" - test.describe("Verifiable Credentials page", () => { - - test("Get offer for test-credential.", async ({ page }) => { - - await login(page, "test-user", "test") - await expect(page.getByTestId("qr-code")).toBeHidden - await page.getByTestId("oid4vci").click - await page.getByTestId("credential-select").click - await expect(page.getByTestId("select-verifiable-credential")).toBeVisible - await expect(page.getByTestId("select-natural-person")).toBeVisible - await page.getByTestId("select-natural-person").click - await expect(page.getByTestId("qr-code")).toBeVisible - }) -}) \ No newline at end of file + test("Get offer for test-credential.", async ({ page }) => { + await login(page, "test-user", "test"); + await expect(page.getByTestId("qr-code")).toBeHidden(); + await page.getByTestId("oid4vci").click(); + await page.getByTestId("credential-select").click(); + await expect( + page.getByTestId("select-verifiable-credential"), + ).toBeVisible(); + await expect(page.getByTestId("select-natural-person")).toBeVisible(); + await page.getByTestId("select-natural-person").click(); + await expect(page.getByTestId("qr-code")).toBeVisible(); + }); +});