From 5ae0e0a645b80b5141ed5c86b91f3707403537de Mon Sep 17 00:00:00 2001 From: forkimenjeckayang <104195313+forkimenjeckayang@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:45:34 +0100 Subject: [PATCH] [OID4VCI] Add Essential OID4VCI Client Scope Configuration Fields to Admin UI (#44389) Closes: #43902 Signed-off-by: forkimenjeckayang --- .../admin/messages/messages_en.properties | 15 +- .../src/client-scopes/details/ScopeForm.tsx | 180 +++++++++- .../client-scope/oid4vci-client-scope.spec.ts | 336 +++++++++++++++--- 3 files changed, 465 insertions(+), 66 deletions(-) diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 1a0657c2465..30bb956c520 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3616,7 +3616,20 @@ issuerDidHelp=The Decentralized Identifier (DID) of the credential issuer. This credentialLifetime=Credential Lifetime (seconds) credentialLifetimeHelp=The lifetime of the credential in seconds. After this time, the credential will expire and become invalid. supportedFormats=Supported Formats -supportedFormatsHelp=The format of the verifiable credential. Currently supported formats: SD-JWT VC (dc+sd-jwt), JWT VC (jwt_vc_json). +supportedFormatsHelp=The format of the verifiable credential. Currently supported formats: SD-JWT VC (dc+sd-jwt), JWT VC (jwt_vc). +credentialDisplay=Credential Display +credentialDisplayHelp=JSON array of objects containing display metadata for wallets (name, logo, colors, etc.). Example: [{"name": "IdentityCredential", "locale": "en-US", "logo": {"uri": "https://example.com/logo.png", "alt_text": "Logo"}, "background_color": "#12107c", "text_color": "#FFFFFF"}] +supportedCredentialTypes=Supported Credential Types +supportedCredentialTypesHelp=Comma-separated list of credential types (e.g., "VerifiableCredential,UniversityDegreeCredential"). Used in the credential definition for JWT VC and SD-JWT formats. +verifiableCredentialType=Verifiable Credential Type (VCT) +verifiableCredentialTypeHelp=The credential type identifier for SD-JWT format credentials. This value is used in the vct claim of the issued credential. Required for SD-JWT format. +tokenJwsType=Token JWS Type +tokenJwsTypeHelp=The type value written into the typ header of the JWT. Defaults to "JWS". Can be set to custom values like "dc+sd-jwt" if required by the wallet or system. +visibleClaims=Visible Claims +visibleClaimsHelp=Comma-separated list of claims that are always disclosed in the SD-JWT body (e.g., "id,iat,nbf,exp,jti,given_name"). Defaults to "id,iat,nbf,exp,jti". Only applicable for SD-JWT format. +signingKeyId=Signing Key ID +signingKeyIdHelp=Optional. The ID of the realm key used to sign the credential. If not specified, the realm's active signing key will be used automatically. +useDefaultKey=Use default (realm's active signing key) # Workflows workflows=Workflows titleWorkflows=Workflows diff --git a/js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx b/js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx index b77c25c5e32..ab6d17c80f6 100644 --- a/js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx +++ b/js/apps/admin-ui/src/client-scopes/details/ScopeForm.tsx @@ -1,6 +1,7 @@ import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; +import type { KeyMetadataRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/keyMetadataRepresentation"; import { ActionGroup, Button } from "@patternfly/react-core"; -import { useEffect } from "react"; +import { useEffect, useMemo, useState } from "react"; import { FormProvider, useForm, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; @@ -9,8 +10,10 @@ import { SelectControl, TextAreaControl, TextControl, + useFetch, } from "@keycloak/keycloak-ui-shared"; +import { useAdminClient } from "../../admin-client"; import { getProtocolName } from "../../clients/utils"; import { DefaultSwitchControl } from "../../components/SwitchControl"; import { @@ -19,12 +22,33 @@ import { } from "../../components/client-scope/ClientScopeTypes"; import { FormAccess } from "../../components/form/FormAccess"; import { useRealm } from "../../context/realm-context/RealmContext"; -import { useLoginProviders } from "../../context/server-info/ServerInfoProvider"; +import { + useLoginProviders, + useServerInfo, +} from "../../context/server-info/ServerInfoProvider"; import { convertAttributeNameToForm, convertToFormValues } from "../../util"; import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled"; import { toClientScopes } from "../routes/ClientScopes"; const OID4VC_PROTOCOL = "oid4vc"; +const VC_FORMAT_JWT_VC = "jwt_vc"; +const VC_FORMAT_SD_JWT = "dc+sd-jwt"; + +// Validation function for comma-separated lists +const validateCommaSeparatedList = (value: string | undefined) => { + if (!value || value.trim() === "") { + return true; + } + if (value.includes(", ") || value.includes(" ,")) { + return "Comma-separated list must not contain spaces around commas"; + } + const entries = value.split(","); + const hasEmptyEntries = entries.some((entry) => entry.trim() === ""); + if (hasEmptyEntries) { + return "Comma-separated list contains empty entries"; + } + return true; +}; type ScopeFormProps = { clientScope?: ClientScopeRepresentation; @@ -33,15 +57,60 @@ type ScopeFormProps = { export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => { const { t } = useTranslation(); + const { adminClient } = useAdminClient(); const form = useForm({ mode: "onChange" }); const { control, handleSubmit, setValue, formState } = form; const { isDirty, isValid } = formState; const { realm } = useRealm(); const providers = useLoginProviders(); + const serverInfo = useServerInfo(); const isFeatureEnabled = useIsFeatureEnabled(); const isDynamicScopesEnabled = isFeatureEnabled(Feature.DynamicScopes); + // Get available signature algorithms from server info + const signatureAlgorithms = useMemo( + () => + serverInfo?.providers?.signature?.providers + ? Object.keys(serverInfo.providers.signature.providers) + : [], + [serverInfo], + ); + + // Fetch realm keys for signing_key_id dropdown + const [realmKeys, setRealmKeys] = useState([]); + + useFetch( + async () => { + const keysMetadata = await adminClient.realms.getKeys({ realm }); + return keysMetadata.keys || []; + }, + setRealmKeys, + [], + ); + + // Prepare key options for SelectControl + // Filter only active keys suitable for signing credentials + const keyOptions = useMemo(() => { + const options = [{ key: "", value: t("useDefaultKey") }]; + if (realmKeys && realmKeys.length > 0) { + const keyOptions = realmKeys + .filter( + (key) => + key.kid && + key.status === "ACTIVE" && + key.algorithm && + signatureAlgorithms.includes(key.algorithm), + ) + .map((key) => ({ + key: key.kid!, + value: `${key.kid} (${key.algorithm})`, + })); + options.push(...keyOptions); + } + return options; + }, [realmKeys, signatureAlgorithms, t]); + const displayOnConsentScreen: string = useWatch({ control, name: convertAttributeNameToForm("attributes.display.on.consent.screen"), @@ -62,6 +131,14 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => { name: "protocol", }); + const selectedFormat = useWatch({ + control, + name: convertAttributeNameToForm( + "attributes.vc.format", + ), + defaultValue: clientScope?.attributes?.["vc.format"] ?? VC_FORMAT_SD_JWT, + }); + const isOid4vcProtocol = selectedProtocol === OID4VC_PROTOCOL; const isOid4vcEnabled = isFeatureEnabled(Feature.OpenId4VCI); const isNotSaml = selectedProtocol != "saml"; @@ -251,13 +328,104 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => { )} label={t("supportedFormats")} labelIcon={t("supportedFormatsHelp")} - controller={{ defaultValue: "dc+sd-jwt" }} + controller={{ defaultValue: VC_FORMAT_SD_JWT }} options={[ - { key: "dc+sd-jwt", value: "SD-JWT VC (dc+sd-jwt)" }, - { key: "jwt_vc", value: "JWT VC (jwt_vc)" }, - { key: "ldp_vc", value: "LDP VC (ldp_vc)" }, + { + key: VC_FORMAT_SD_JWT, + value: `SD-JWT VC (${VC_FORMAT_SD_JWT})`, + }, + { + key: VC_FORMAT_JWT_VC, + value: `JWT VC (${VC_FORMAT_JWT_VC})`, + }, ]} /> + ( + "attributes.vc.credential_build_config.token_jws_type", + )} + label={t("tokenJwsType")} + labelIcon={t("tokenJwsTypeHelp")} + defaultValue={ + clientScope?.attributes?.[ + "vc.credential_build_config.token_jws_type" + ] ?? "JWS" + } + /> + {realmKeys && realmKeys.length > 0 && ( + ( + "attributes.vc.signing_key_id", + )} + label={t("signingKeyId")} + labelIcon={t("signingKeyIdHelp")} + controller={{ + defaultValue: + clientScope?.attributes?.["vc.signing_key_id"] ?? "", + }} + options={keyOptions} + /> + )} + ( + "attributes.vc.display", + )} + label={t("credentialDisplay")} + labelIcon={t("credentialDisplayHelp")} + rules={{ + validate: (value: string | undefined) => { + if (!value || value.trim() === "") { + return true; + } + try { + JSON.parse(value); + return true; + } catch { + return "Invalid JSON format"; + } + }, + }} + /> + {(selectedFormat === VC_FORMAT_JWT_VC || + selectedFormat === VC_FORMAT_SD_JWT) && ( + ( + "attributes.vc.supported_credential_types", + )} + label={t("supportedCredentialTypes")} + labelIcon={t("supportedCredentialTypesHelp")} + rules={{ + validate: validateCommaSeparatedList, + }} + /> + )} + {selectedFormat === VC_FORMAT_SD_JWT && ( + <> + ( + "attributes.vc.verifiable_credential_type", + )} + label={t("verifiableCredentialType")} + labelIcon={t("verifiableCredentialTypeHelp")} + /> + ( + "attributes.vc.credential_build_config.sd_jwt.visible_claims", + )} + label={t("visibleClaims")} + labelIcon={t("visibleClaimsHelp")} + defaultValue={ + clientScope?.attributes?.[ + "vc.credential_build_config.sd_jwt.visible_claims" + ] ?? "id,iat,nbf,exp,jti" + } + rules={{ + validate: validateCommaSeparatedList, + }} + /> + + )} )} diff --git a/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts b/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts index feeb2e40016..3c8e3ba9c24 100644 --- a/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts +++ b/js/apps/admin-ui/test/client-scope/oid4vci-client-scope.spec.ts @@ -1,11 +1,63 @@ import { expect, test } from "@playwright/test"; +import type { Page } from "@playwright/test"; import { createTestBed } from "../support/testbed.ts"; import { goToClientScopes } from "../utils/sidebar.ts"; -import { clickSaveButton } from "../utils/form.ts"; +import { clickSaveButton, selectItem } from "../utils/form.ts"; import { clickTableRowItem, clickTableToolbarItem } from "../utils/table.ts"; import { login } from "../utils/login.ts"; import { toClientScopes } from "../../src/client-scopes/routes/ClientScopes.tsx"; +// Helper function to create client scope (without selecting protocol) +async function createClientScope( + page: Page, + testBed: Awaited>, +) { + await login(page, { to: toClientScopes({ realm: testBed.realm }) }); + + await goToClientScopes(page); + await page.waitForLoadState("domcontentloaded"); + + await clickTableToolbarItem(page, "Create client scope"); + await page.waitForLoadState("domcontentloaded"); +} + +// Helper function to create client scope and select protocol/format +async function createClientScopeAndSelectProtocolAndFormat( + page: Page, + testBed: Awaited>, + format?: "SD-JWT VC (dc+sd-jwt)" | "JWT VC (jwt_vc)", +) { + await createClientScope(page, testBed); + + await selectItem(page, "#kc-protocol", "OpenID for Verifiable Credentials"); + + await page.waitForLoadState("domcontentloaded"); + + if (format) { + await selectItem(page, "#kc-vc-format", format); + await page.waitForLoadState("domcontentloaded"); + } +} + +// Helper function to navigate back to client scope and verify saved values +async function navigateBackAndVerifyClientScope( + page: Page, + testBed: Awaited>, + clientScopeName: string, +) { + const currentUrl = page.url(); + const baseUrl = currentUrl.split("#")[0]; + await page.goto( + `${baseUrl}#${toClientScopes({ realm: testBed.realm }).pathname!}`, + ); + await page.waitForLoadState("domcontentloaded"); + + await page.getByPlaceholder("Search for client scope").fill(clientScopeName); + + await clickTableRowItem(page, clientScopeName); + await page.waitForLoadState("domcontentloaded"); +} + // OID4VCI field selectors const OID4VCI_FIELDS = { CREDENTIAL_CONFIGURATION_ID: "attributes.vc🍺credential_configuration_id", @@ -13,6 +65,13 @@ const OID4VCI_FIELDS = { ISSUER_DID: "attributes.vc🍺issuer_did", EXPIRY_IN_SECONDS: "attributes.vc🍺expiry_in_seconds", FORMAT: "#kc-vc-format", + TOKEN_JWS_TYPE: "attributes.vc🍺credential_build_config🍺token_jws_type", + SIGNING_KEY_ID: "#kc-signing-key-id", + DISPLAY: "attributes.vc🍺display", + SUPPORTED_CREDENTIAL_TYPES: "attributes.vc🍺supported_credential_types", + VERIFIABLE_CREDENTIAL_TYPE: "attributes.vc🍺verifiable_credential_type", + VISIBLE_CLAIMS: + "attributes.vc🍺credential_build_config🍺sd_jwt🍺visible_claims", } as const; // Test values @@ -21,7 +80,12 @@ const TEST_VALUES = { CREDENTIAL_ID: "test-cred-identifier", ISSUER_DID: "did:key:test123", EXPIRY_SECONDS: "86400", - FORMAT: "jwt_vc", + TOKEN_JWS_TYPE: "dc+sd-jwt", + VISIBLE_CLAIMS: "id,iat,nbf,exp,jti,given_name", + DISPLAY: + '[{"name": "Test Credential", "locale": "en-US", "logo": {"uri": "https://example.com/logo.png", "alt_text": "Logo"}, "background_color": "#12107c", "text_color": "#FFFFFF"}]', + SUPPORTED_CREDENTIAL_TYPES: "VerifiableCredential,UniversityDegreeCredential", + VERIFIABLE_CREDENTIAL_TYPE: "TestCredentialType", } as const; test.describe("OID4VCI Client Scope Functionality", () => { @@ -29,13 +93,7 @@ test.describe("OID4VCI Client Scope Functionality", () => { page, }) => { await using testBed = await createTestBed(); - await login(page, { to: toClientScopes({ realm: testBed.realm }) }); - - await goToClientScopes(page); - await page.waitForLoadState("domcontentloaded"); - - await clickTableToolbarItem(page, "Create client scope"); - await page.waitForLoadState("domcontentloaded"); + await createClientScope(page, testBed); await expect(page.locator("#kc-protocol")).toBeVisible(); @@ -65,26 +123,19 @@ test.describe("OID4VCI Client Scope Functionality", () => { page.getByTestId(OID4VCI_FIELDS.EXPIRY_IN_SECONDS), ).toBeVisible(); await expect(page.locator(OID4VCI_FIELDS.FORMAT)).toBeVisible(); + await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible(); + await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeVisible(); }); test("should save and persist OID4VCI field values", async ({ page }) => { await using testBed = await createTestBed(); const testClientScopeName = `oid4vci-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; - await login(page, { to: toClientScopes({ realm: testBed.realm }) }); - - await goToClientScopes(page); - await page.waitForLoadState("domcontentloaded"); - - await clickTableToolbarItem(page, "Create client scope"); - await page.waitForLoadState("domcontentloaded"); - - await expect(page.locator("#kc-protocol")).toBeVisible(); - - const { selectItem } = await import("../utils/form.ts"); - await selectItem(page, "#kc-protocol", "OpenID for Verifiable Credentials"); - - await page.waitForLoadState("domcontentloaded"); + await createClientScopeAndSelectProtocolAndFormat( + page, + testBed, + "JWT VC (jwt_vc)", + ); await page .getByTestId(OID4VCI_FIELDS.CREDENTIAL_CONFIGURATION_ID) @@ -99,26 +150,21 @@ test.describe("OID4VCI Client Scope Functionality", () => { .getByTestId(OID4VCI_FIELDS.EXPIRY_IN_SECONDS) .fill(TEST_VALUES.EXPIRY_SECONDS); - await selectItem(page, "#kc-vc-format", "JWT VC (jwt_vc)"); + await page + .getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE) + .fill(TEST_VALUES.TOKEN_JWS_TYPE); + + await page.getByTestId(OID4VCI_FIELDS.DISPLAY).fill(TEST_VALUES.DISPLAY); + await page + .getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES) + .fill(TEST_VALUES.SUPPORTED_CREDENTIAL_TYPES); await page.getByTestId("name").fill(testClientScopeName); await clickSaveButton(page); await expect(page.getByText("Client scope created")).toBeVisible(); - const currentUrl = page.url(); - const baseUrl = currentUrl.split("#")[0]; - await page.goto( - `${baseUrl}#${toClientScopes({ realm: testBed.realm }).pathname!}`, - ); - await page.waitForLoadState("domcontentloaded"); - - await page - .getByPlaceholder("Search for client scope") - .fill(testClientScopeName); - - await clickTableRowItem(page, testClientScopeName); - await page.waitForLoadState("domcontentloaded"); + await navigateBackAndVerifyClientScope(page, testBed, testClientScopeName); await expect( page.getByTestId(OID4VCI_FIELDS.CREDENTIAL_CONFIGURATION_ID), @@ -135,20 +181,22 @@ test.describe("OID4VCI Client Scope Functionality", () => { await expect(page.locator("#kc-vc-format")).toContainText( "JWT VC (jwt_vc)", ); + await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toHaveValue( + TEST_VALUES.DISPLAY, + ); + await expect( + page.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES), + ).toHaveValue(TEST_VALUES.SUPPORTED_CREDENTIAL_TYPES); + await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toHaveValue( + TEST_VALUES.TOKEN_JWS_TYPE, + ); }); test("should show OID4VCI protocol when global feature is enabled", async ({ page, }) => { await using testBed = await createTestBed(); - - await login(page, { to: toClientScopes({ realm: testBed.realm }) }); - - await goToClientScopes(page); - await page.waitForLoadState("domcontentloaded"); - - await clickTableToolbarItem(page, "Create client scope"); - await page.waitForLoadState("domcontentloaded"); + await createClientScope(page, testBed); await expect(page.locator("#kc-protocol")).toBeVisible(); @@ -163,13 +211,7 @@ test.describe("OID4VCI Client Scope Functionality", () => { page, }) => { await using testBed = await createTestBed(); - await login(page, { to: toClientScopes({ realm: testBed.realm }) }); - - await goToClientScopes(page); - await page.waitForLoadState("domcontentloaded"); - - await clickTableToolbarItem(page, "Create client scope"); - await page.waitForLoadState("domcontentloaded"); + await createClientScope(page, testBed); await expect(page.locator("#kc-protocol")).toBeVisible(); @@ -195,19 +237,14 @@ test.describe("OID4VCI Client Scope Functionality", () => { page.getByTestId(OID4VCI_FIELDS.EXPIRY_IN_SECONDS), ).toBeHidden(); await expect(page.locator(OID4VCI_FIELDS.FORMAT)).toBeHidden(); + await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeHidden(); }); test("should handle OID4VCI protocol selection correctly", async ({ page, }) => { await using testBed = await createTestBed(); - await login(page, { to: toClientScopes({ realm: testBed.realm }) }); - - await goToClientScopes(page); - await page.waitForLoadState("domcontentloaded"); - - await clickTableToolbarItem(page, "Create client scope"); - await page.waitForLoadState("domcontentloaded"); + await createClientScope(page, testBed); await expect(page.locator("#kc-protocol")).toBeVisible(); @@ -236,4 +273,185 @@ test.describe("OID4VCI Client Scope Functionality", () => { page.getByTestId(OID4VCI_FIELDS.CREDENTIAL_CONFIGURATION_ID), ).toBeVisible(); }); + + test("should only show supported format options (dc+sd-jwt and jwt_vc)", async ({ + page, + }) => { + await using testBed = await createTestBed(); + await createClientScopeAndSelectProtocolAndFormat(page, testBed); + + await page.locator("#kc-vc-format").click(); + + await expect( + page.getByRole("option", { name: "SD-JWT VC (dc+sd-jwt)" }), + ).toBeVisible(); + await expect( + page.getByRole("option", { name: "JWT VC (jwt_vc)" }), + ).toBeVisible(); + + await expect( + page.getByRole("option", { name: "LDP VC (ldp_vc)" }), + ).toBeHidden(); + }); + + test("should show format-specific fields for SD-JWT format", async ({ + page, + }) => { + await using testBed = await createTestBed(); + await createClientScopeAndSelectProtocolAndFormat( + page, + testBed, + "SD-JWT VC (dc+sd-jwt)", + ); + + await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeVisible(); + await expect( + page.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES), + ).toBeVisible(); + + await expect( + page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE), + ).toBeVisible(); + await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeVisible(); + }); + + test("should show format-specific fields for JWT VC format", async ({ + page, + }) => { + await using testBed = await createTestBed(); + await createClientScopeAndSelectProtocolAndFormat( + page, + testBed, + "JWT VC (jwt_vc)", + ); + + await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toBeVisible(); + await expect( + page.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES), + ).toBeVisible(); + + await expect( + page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE), + ).toBeHidden(); + await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeHidden(); + }); + + test("should save and persist new OID4VCI field values for SD-JWT format", async ({ + page, + }) => { + await using testBed = await createTestBed(); + const testClientScopeName = `oid4vci-sdjwt-test-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; + + await createClientScopeAndSelectProtocolAndFormat( + page, + testBed, + "SD-JWT VC (dc+sd-jwt)", + ); + + await page + .getByTestId(OID4VCI_FIELDS.CREDENTIAL_CONFIGURATION_ID) + .fill(TEST_VALUES.CREDENTIAL_CONFIG); + await page + .getByTestId(OID4VCI_FIELDS.CREDENTIAL_IDENTIFIER) + .fill(TEST_VALUES.CREDENTIAL_ID); + await page.getByTestId(OID4VCI_FIELDS.DISPLAY).fill(TEST_VALUES.DISPLAY); + await page + .getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES) + .fill(TEST_VALUES.SUPPORTED_CREDENTIAL_TYPES); + await page + .getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE) + .fill(TEST_VALUES.VERIFIABLE_CREDENTIAL_TYPE); + await page + .getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS) + .fill(TEST_VALUES.VISIBLE_CLAIMS); + + await page.getByTestId("name").fill(testClientScopeName); + + await clickSaveButton(page); + await expect(page.getByText("Client scope created")).toBeVisible(); + + await navigateBackAndVerifyClientScope(page, testBed, testClientScopeName); + + await expect(page.getByTestId(OID4VCI_FIELDS.DISPLAY)).toHaveValue( + TEST_VALUES.DISPLAY, + ); + await expect( + page.getByTestId(OID4VCI_FIELDS.SUPPORTED_CREDENTIAL_TYPES), + ).toHaveValue(TEST_VALUES.SUPPORTED_CREDENTIAL_TYPES); + await expect( + page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE), + ).toHaveValue(TEST_VALUES.VERIFIABLE_CREDENTIAL_TYPE); + await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toHaveValue( + TEST_VALUES.VISIBLE_CLAIMS, + ); + await expect(page.locator("#kc-vc-format")).toContainText( + "SD-JWT VC (dc+sd-jwt)", + ); + }); + + test("should conditionally show/hide fields when format changes", async ({ + page, + }) => { + await using testBed = await createTestBed(); + await createClientScopeAndSelectProtocolAndFormat( + page, + testBed, + "SD-JWT VC (dc+sd-jwt)", + ); + + await expect( + page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE), + ).toBeVisible(); + + await selectItem(page, "#kc-vc-format", "JWT VC (jwt_vc)"); + + await page.waitForLoadState("domcontentloaded"); + + await expect( + page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE), + ).toBeHidden(); + + await selectItem(page, "#kc-vc-format", "SD-JWT VC (dc+sd-jwt)"); + + await page.waitForLoadState("domcontentloaded"); + + await expect( + page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE), + ).toBeVisible(); + await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeVisible(); + + await selectItem(page, "#kc-vc-format", "JWT VC (jwt_vc)"); + + await page.waitForLoadState("domcontentloaded"); + + await expect( + page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE), + ).toBeHidden(); + await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeHidden(); + + await selectItem(page, "#kc-vc-format", "SD-JWT VC (dc+sd-jwt)"); + + await page.waitForLoadState("domcontentloaded"); + + await expect( + page.getByTestId(OID4VCI_FIELDS.VERIFIABLE_CREDENTIAL_TYPE), + ).toBeVisible(); + await expect(page.getByTestId(OID4VCI_FIELDS.VISIBLE_CLAIMS)).toBeVisible(); + }); + + test("should show token_jws_type for all formats", async ({ page }) => { + await using testBed = await createTestBed(); + await createClientScopeAndSelectProtocolAndFormat( + page, + testBed, + "JWT VC (jwt_vc)", + ); + + await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible(); + + await selectItem(page, "#kc-vc-format", "SD-JWT VC (dc+sd-jwt)"); + await page.waitForLoadState("domcontentloaded"); + + await expect(page.getByTestId(OID4VCI_FIELDS.TOKEN_JWS_TYPE)).toBeVisible(); + }); });