[OID4VCI] Add Essential OID4VCI Client Scope Configuration Fields to Admin UI (#44389)

Closes: #43902


Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
This commit is contained in:
forkimenjeckayang 2025-12-01 15:45:34 +01:00 committed by GitHub
parent 9a6a7d98b1
commit 5ae0e0a645
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 465 additions and 66 deletions

View File

@ -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

View File

@ -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<ClientScopeDefaultOptionalType>({ 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<KeyMetadataRepresentation[]>([]);
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<ClientScopeDefaultOptionalType>(
"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})`,
},
]}
/>
<TextControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"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 && (
<SelectControl
id="kc-signing-key-id"
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.vc.signing_key_id",
)}
label={t("signingKeyId")}
labelIcon={t("signingKeyIdHelp")}
controller={{
defaultValue:
clientScope?.attributes?.["vc.signing_key_id"] ?? "",
}}
options={keyOptions}
/>
)}
<TextAreaControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"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) && (
<TextControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.vc.supported_credential_types",
)}
label={t("supportedCredentialTypes")}
labelIcon={t("supportedCredentialTypesHelp")}
rules={{
validate: validateCommaSeparatedList,
}}
/>
)}
{selectedFormat === VC_FORMAT_SD_JWT && (
<>
<TextControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"attributes.vc.verifiable_credential_type",
)}
label={t("verifiableCredentialType")}
labelIcon={t("verifiableCredentialTypeHelp")}
/>
<TextControl
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
"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,
}}
/>
</>
)}
</>
)}

View File

@ -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<ReturnType<typeof 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");
}
// Helper function to create client scope and select protocol/format
async function createClientScopeAndSelectProtocolAndFormat(
page: Page,
testBed: Awaited<ReturnType<typeof createTestBed>>,
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<ReturnType<typeof createTestBed>>,
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();
});
});