From e98c1b9e4b18c9b8f2470e7664d4755781aa6f38 Mon Sep 17 00:00:00 2001 From: Rolson Quadras Date: Thu, 1 Aug 2024 10:01:31 -0400 Subject: [PATCH] feat(sdk): New Display API to get all locale data (#797) Signed-off-by: Rolson Quadras --- cmd/wallet-sdk-gomobile/display/credential.go | 288 ++++++++++++++++++ cmd/wallet-sdk-gomobile/display/resolve.go | 16 + .../display/resolve_test.go | 41 +++ cmd/wallet-sdk-gomobile/docs/usage.md | 115 ++++++- pkg/credentialschema/credentialdisplay.go | 161 ++++++++++ pkg/credentialschema/credentialschema.go | 26 ++ pkg/credentialschema/credentialschema_test.go | 19 ++ pkg/credentialschema/issuerdisplay.go | 17 ++ pkg/credentialschema/models.go | 32 ++ 9 files changed, 714 insertions(+), 1 deletion(-) create mode 100644 cmd/wallet-sdk-gomobile/display/credential.go diff --git a/cmd/wallet-sdk-gomobile/display/credential.go b/cmd/wallet-sdk-gomobile/display/credential.go new file mode 100644 index 00000000..a32e31bd --- /dev/null +++ b/cmd/wallet-sdk-gomobile/display/credential.go @@ -0,0 +1,288 @@ +/* +Copyright Gen Digital Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +// Package display contains functionality that can be used to resolve display values per the OpenID4CI spec. +package display + +import ( + "encoding/json" + "errors" + + goapicredentialschema "github.com/trustbloc/wallet-sdk/pkg/credentialschema" +) + +// Issuer represents display information about the issuer of some credential(s). +type Issuer struct { + issuerDisplay *goapicredentialschema.ResolvedIssuerDisplay +} + +// Serialize serializes this IssuerDisplay object into JSON. +func (d *Issuer) Serialize() (string, error) { + issuerDisplayBytes, err := json.Marshal(d.issuerDisplay) + + return string(issuerDisplayBytes), err +} + +// Name returns the issuer's display name. +func (d *Issuer) Name() string { + return d.issuerDisplay.Name +} + +// Locale returns the locale corresponding to this issuer's display name. +// The locale is determined during the ResolveDisplay call based on the preferred locale passed in and what +// localizations were provided in the issuer's metadata. +func (d *Issuer) Locale() string { + return d.issuerDisplay.Locale +} + +// URL returns this IssuerDisplay's URL. +func (d *Issuer) URL() string { + return d.issuerDisplay.URL +} + +// Logo returns this IssuerDisplay's logo. +// If it has no logo, then nil/null is returned instead. +func (d *Issuer) Logo() *Logo { + if d.issuerDisplay.Logo == nil { + return nil + } + + return &Logo{logo: d.issuerDisplay.Logo} +} + +// BackgroundColor returns this LocalizedIssuerDisplay's background color. +func (d *Issuer) BackgroundColor() string { + return d.issuerDisplay.BackgroundColor +} + +// TextColor returns this IssuerDisplay's text color. +func (d *Issuer) TextColor() string { + return d.issuerDisplay.TextColor +} + +// Resolved represents display information for all locales for the issued credentials based on an issuer's metadata. +type Resolved struct { + resolvedDisplayData *goapicredentialschema.ResolvedData +} + +// ParseResolvedData parses the given serialized display data and returns a display Data object. +func ParseResolvedData(displayData string) (*Resolved, error) { + var parsedDisplayData goapicredentialschema.ResolvedData + + err := json.Unmarshal([]byte(displayData), &parsedDisplayData) + if err != nil { + return nil, err + } + + return &Resolved{resolvedDisplayData: &parsedDisplayData}, nil +} + +// Serialize serializes this display Data object into JSON. +func (d *Resolved) Serialize() (string, error) { + resolvedDisplayDataBytes, err := json.Marshal(d.resolvedDisplayData) + + return string(resolvedDisplayDataBytes), err +} + +// LocalizedIssuersLength returns the number of different locales supported for the issuer displays. +func (d *Resolved) LocalizedIssuersLength() int { + return len(d.resolvedDisplayData.LocalizedIssuer) +} + +// LocalizedIssuerAtIndex returns the issuer display object at the given index. +// If the index passed in is out of bounds, then nil is returned. +func (d *Resolved) LocalizedIssuerAtIndex(index int) *Issuer { + maxIndex := len(d.resolvedDisplayData.LocalizedIssuer) - 1 + if index > maxIndex || index < 0 { + return nil + } + + return &Issuer{issuerDisplay: &d.resolvedDisplayData.LocalizedIssuer[index]} +} + +// CredentialsLength returns the number of credential displays contained within this display Data object. +func (d *Resolved) CredentialsLength() int { + return len(d.resolvedDisplayData.Credential) +} + +// CredentialAtIndex returns the credential display object at the given index. +// If the index passed in is out of bounds, then nil is returned. +func (d *Resolved) CredentialAtIndex(index int) *Credential { + maxIndex := len(d.resolvedDisplayData.Credential) - 1 + if index > maxIndex || index < 0 { + return nil + } + + return &Credential{credentialDisplay: &d.resolvedDisplayData.Credential[index]} +} + +// Overview represents display data for a credential as a whole. +type Overview struct { + overview *goapicredentialschema.CredentialOverview +} + +// Name returns the display name for the credential. +func (c *Overview) Name() string { + return c.overview.Name +} + +// Logo returns display logo data for the credential. +func (c *Overview) Logo() *Logo { + return &Logo{logo: c.overview.Logo} +} + +// BackgroundColor returns the background color that should be used when displaying this credential. +func (c *Overview) BackgroundColor() string { + return c.overview.BackgroundColor +} + +// TextColor returns the text color that should be used when displaying this credential. +func (c *Overview) TextColor() string { + return c.overview.TextColor +} + +// Locale returns the locale corresponding to this credential overview's display data. +// The locale is determined during the ResolveDisplay call based on the preferred locale passed in and what +// localizations were provided in the issuer's metadata. +func (c *Overview) Locale() string { + return c.overview.Locale +} + +// Credential represents display data for a credential. +type Credential struct { + credentialDisplay *goapicredentialschema.Credential +} + +// LocalizedOverviewsLength returns the number of different locales supported for the credential displays. +func (d *Credential) LocalizedOverviewsLength() int { + return len(d.credentialDisplay.LocalizedOverview) +} + +// LocalizedOverviewAtIndex returns the number of different locales supported for the issuer displays. +// If the index passed in is out of bounds, then nil is returned. +func (d *Credential) LocalizedOverviewAtIndex(index int) *Overview { + maxIndex := len(d.credentialDisplay.LocalizedOverview) - 1 + if index > maxIndex || index < 0 { + return nil + } + + return &Overview{overview: &d.credentialDisplay.LocalizedOverview[index]} +} + +// SubjectsLength returns the number of credential subject displays contained within this Credential object. +func (c *Credential) SubjectsLength() int { + return len(c.credentialDisplay.Subject) +} + +// SubjectAtIndex returns the credential subject display object at the given index. +// If the index passed in is out of bounds, then nil is returned. +func (c *Credential) SubjectAtIndex(index int) *Subject { + maxIndex := len(c.credentialDisplay.Subject) - 1 + if index > maxIndex || index < 0 { + return nil + } + + return &Subject{claim: &c.credentialDisplay.Subject[index]} +} + +// Subject represents display data for a specific credential subject including all locales for the label. +type Subject struct { + claim *goapicredentialschema.Subject +} + +// RawID returns the raw field name (key) from the VC associated with this claim. +// It's not localized or formatted for display. +func (c *Subject) RawID() string { + return c.claim.RawID +} + +// LocalizedLabelsLength returns the number of different locales supported for the credential subject. +func (c *Subject) LocalizedLabelsLength() int { + return len(c.claim.LocalizedLabels) +} + +// LocalizedLabelAtIndex returns the label at the given index. +// If the index passed in is out of bounds, then nil is returned. +func (c *Subject) LocalizedLabelAtIndex(index int) *Label { + maxIndex := len(c.claim.LocalizedLabels) - 1 + if index > maxIndex || index < 0 { + return nil + } + + return &Label{label: &c.claim.LocalizedLabels[index]} +} + +// ValueType returns the display value type for this claim. +// For example: "string", "number", "image", "attachment" etc. +// For type=attachment, ignore the RawValue() and Value(), instead use Attachment() method. +func (c *Subject) ValueType() string { + return c.claim.ValueType +} + +// Value returns the display value for this claim. +// For example, if the UI were to display "Given Name: Alice", then the Value would be "Alice". +// If no special formatting was applied to the display value, then this method will be equivalent to calling RawValue. +func (c *Subject) Value() string { + if c.claim.Value == nil { + return c.claim.RawValue + } + + return *c.claim.Value +} + +// RawValue returns the raw display value for this claim without any formatting. +// For example, if this claim is masked, this method will return the unmasked version. +// If no special formatting was applied to the display value, then this method will be equivalent to calling Value. +func (c *Subject) RawValue() string { + return c.claim.RawValue +} + +// IsMasked indicates whether this claim's value is masked. If this method returns true, then the Value method +// will return the masked value while the RawValue method will return the unmasked version. +func (c *Subject) IsMasked() bool { + return c.claim.Mask != "" +} + +// Pattern returns the pattern information for this claim. +func (c *Subject) Pattern() string { + return c.claim.Pattern +} + +// HasOrder returns whether this Claim has a specified order in it. +func (c *Subject) HasOrder() bool { + return c.claim.Order != nil +} + +// Order returns the display order for this claim. +// HasOrder should be called first to ensure this claim has a specified order before calling this method. +// This method returns an error if the claim has no specified order. +func (c *Subject) Order() (int, error) { + if c.claim.Order == nil { + return -1, errors.New("claim has no specified order") + } + + return *c.claim.Order, nil +} + +// Attachment returns the attachment data. Check this field if the claim type is "attachment", instead of value field. +func (c *Subject) Attachment() *Attachment { + return &Attachment{attachment: c.claim.Attachment} +} + +// Label represents localized name and locale.. +type Label struct { + label *goapicredentialschema.Label +} + +// Name in a locale. +func (c *Label) Name() string { + return c.label.Name +} + +// Locale locale value. +func (c *Label) Locale() string { + return c.label.Locale +} diff --git a/cmd/wallet-sdk-gomobile/display/resolve.go b/cmd/wallet-sdk-gomobile/display/resolve.go index 72416928..360a1e2f 100644 --- a/cmd/wallet-sdk-gomobile/display/resolve.go +++ b/cmd/wallet-sdk-gomobile/display/resolve.go @@ -28,6 +28,8 @@ import ( // The CredentialDisplays in the returned Data object correspond to the VCs passed in and are in the // same order. // This method requires one or more VCs and the issuer's base URI. +// Deprecated: Use ResolveCredential function instead, which would give data for all locales. The consumer of the SDK +// can run logic to display exact locale data to the user. func Resolve(vcs *verifiable.CredentialsArray, issuerURI string, opts *Opts) (*Data, error) { goAPIOpts, err := generateGoAPIOpts(vcs, issuerURI, opts) if err != nil { @@ -42,6 +44,20 @@ func Resolve(vcs *verifiable.CredentialsArray, issuerURI string, opts *Opts) (*D return &Data{resolvedDisplayData: resolvedDisplayData}, nil } +func ResolveCredential(vcs *verifiable.CredentialsArray, issuerURI string, opts *Opts) (*Resolved, error) { + goAPIOpts, err := generateGoAPIOpts(vcs, issuerURI, opts) + if err != nil { + return nil, err + } + + resolvedDisplayData, err := goapicredentialschema.ResolveCredential(goAPIOpts...) + if err != nil { + return nil, err + } + + return &Resolved{resolvedDisplayData: resolvedDisplayData}, nil +} + // ResolveCredentialOffer resolves display information for some offered credentials based on an issuer's metadata. // The CredentialDisplays in the returned ResolvedDisplayData object correspond to the offered credential types // passed in and are in the same order. diff --git a/cmd/wallet-sdk-gomobile/display/resolve_test.go b/cmd/wallet-sdk-gomobile/display/resolve_test.go index b96c6493..434ff6d6 100644 --- a/cmd/wallet-sdk-gomobile/display/resolve_test.go +++ b/cmd/wallet-sdk-gomobile/display/resolve_test.go @@ -203,6 +203,47 @@ func TestResolve(t *testing.T) { }) } +func TestResolveCredential(t *testing.T) { + issuerServerHandler := &mockIssuerServerHandler{ + t: t, + issuerMetadata: string(sampleIssuerMetadata), + } + server := httptest.NewServer(issuerServerHandler) + + defer server.Close() + + parseVCOptionalArgs := verifiable.NewOpts() + parseVCOptionalArgs.DisableProofCheck() + + vc, err := verifiable.ParseCredential(credentialUniversityDegree, parseVCOptionalArgs) + require.NoError(t, err) + + vcs := verifiable.NewCredentialsArray() + vcs.Add(vc) + + opts := display.NewOpts().SetMaskingString("*") + + resolvedDisplayData, err := display.ResolveCredential(vcs, server.URL, opts) + require.NoError(t, err) + + require.Equal(t, resolvedDisplayData.LocalizedIssuersLength(), 2) + require.Equal(t, resolvedDisplayData.CredentialsLength(), 1) + require.Equal(t, resolvedDisplayData.CredentialAtIndex(0).LocalizedOverviewsLength(), 1) + require.Equal(t, resolvedDisplayData.CredentialAtIndex(0).SubjectsLength(), 6) + + credentialDisplay := resolvedDisplayData.CredentialAtIndex(0) + + for i := 0; i < credentialDisplay.SubjectsLength(); i++ { + claim := credentialDisplay.SubjectAtIndex(i) + + if claim.LocalizedLabelAtIndex(0).Name() == sensitiveIDLabel { + require.Equal(t, "*****6789", claim.Value()) + } else if claim.LocalizedLabelAtIndex(0).Name() == reallySensitiveIDLabel { + require.Equal(t, "*******", claim.Value()) + } + } +} + func TestResolveCredentialOffer(t *testing.T) { metadata := &issuer.Metadata{} diff --git a/cmd/wallet-sdk-gomobile/docs/usage.md b/cmd/wallet-sdk-gomobile/docs/usage.md index da98587f..e5caab1f 100644 --- a/cmd/wallet-sdk-gomobile/docs/usage.md +++ b/cmd/wallet-sdk-gomobile/docs/usage.md @@ -21,7 +21,8 @@ For the sake of readability, the following is omitted in most code examples: - [Decentralized Identifier (DID) Resolver](#decentralized-identifier-did-resolver) - [DID Service Validation](#did-service-validation) - [OpenID Credential Issuance (OpenID4VCI)](#openid-credential-issuance-openid4vci) -- [Credential Display API](#credential-display-api) +- [Credential Display API (Deprecated)](#credential-display-api) +- [Credential Display API with Locale](#credential-display-with-locale-api) - [Credential Status](#credential-status) - [OpenID Credential Presentation (OpenID4VP)](#openid-credential-presentation-openid4vp) - [Trust Evaluation](#trust-evaluation) @@ -1090,6 +1091,8 @@ let credentials = interaction.requestCredential(vm: didDocument.assertionMethod( ## Credential Display API +Note: This API has been deprecated. Please use the [Credential Display API with Locale](#credential-display-with-locale-api) API instead. + After completing the `RequestCredential` step of the OpenID4CI flow, you will have your issued Verifiable Credential objects. These objects contain the data needed for various wallet operations, but they don't tell you how you can display the credential data in an easily-understandable way via a user interface. This is where the credential display @@ -1278,6 +1281,116 @@ let opts = DisplayNewOpts().setPreferredLocale("en-us") let displayData = DisplayResolve(vcArray, "Issuer_URI_Goes_Here", opts, &error) ``` +## Credential Display with locale API + +After completing the `RequestCredential` step of the OpenID4CI flow, you will have your issued Verifiable Credential +objects. These objects contain the data needed for various wallet operations, but they don't tell you how you can +display the credential data in an easily-understandable way via a user interface. This is where the credential display +data comes in. + +To get display data with all locales, call the `display.resolveCredential` function with your VCs and the issuer URI. An issuer URI can be obtained by +calling the `issuerURI` method on an OpenID4CI interaction object after it's been instantiated. It's a good idea to +store the issuer URI somewhere in persistent storage after going through the OpenID4CI flow. This way, you can call the +`display.resolveCredential` function later if/when you need to refresh your display data based on the latest display information +from the issuer. Note that if the issuer uses signed metadata, then you'll need to also pass a DID resolver into +the `display.resolveCredential` function. See the [Options](#options) section for more information. + + +### Options + +The `display.resolveCredential` function has a number of different options available. For a full list of available options, check +the associated `Opts` object. This section will highlight some especially notable ones: + +### Set DID Resolver + +If the issuer makes use of signed issuer metadata, then you'll need to pass in a DID resolver using the +`setDIDResolver(didResolver)` option. Otherwise, the`resolveDisplay` call will fail. + +### Set Masking String + +The `setMaskingString(maskingString)` option allows you to specify the string to be used when creating masked values for +display. The substitution is done on a character-by-character basis, whereby each individual character to be masked +will be replaced by the entire string. See the examples below to better understand exactly how the substitution works. + +#### Examples + +Note that any quote characters used in these examples are only there for readability reasons - they're not actually +part of the values. + +Scenario: The unmasked display value is 12345, and the issuer's metadata specifies that the first 3 characters are +to be masked. The most common use-case is to substitute every masked character with a single character. This is +achieved by specifying just a single character in the `maskingString`. Here's what the masked value would look like +with different `maskingString` choices: +``` +maskingString="•" --> •••45 +maskingString="*" --> ***45 +``` + +It's also possible to specify multiple characters in the `maskingString`, or even an empty string if so desired. +Here's what the masked value would like in such cases: + +``` +maskingString="???" --> ?????????45 +maskingString="" --> 45 +``` + +If this option isn't used, then by default "•" characters (without the quotes) will be used for masking. + +### The Display Object Structure + +The structure of the display data object is as follows: + +#### `Resolved` + +* The root object. +* Contains display information for the issuer and each of the credentials passed in to the API. +* Use the `LocalizedIssuersLength()` and `LocalizedIssuerAtIndex()` methods to iterate over the different localized issuer display data. +* Use the `credentialsLength()` and `credentialAtIndex()` methods to iterate over the credential passed to the API. + +#### `Issuer` + +* Describes display information about the issuer. +* Has `name()`, `locale()`, `URL()`, `Logo()`, `BackgroundColor()` and `TextColor()` methods. + +#### `Credential` + +* Describes display information about a credential. +* Use the `localizedOverviewsLength()` and `localizedOverviewAtIndex()` methods to iterate over the different localized credential display data. +* Use the `subjectsLength()` and `subjectAtIndex()` methods to iterate over the credential `Subject` or claim objects. + +#### `Overview` + +* Describes display information for a credential as a whole. +* Has `name()`, `logo()`, `backgroundColor()`, `textColor()`, and `locale()` methods. The `logo()` method returns + a `Logo` object. + +#### `Logo` + +* Describes display information for a logo. +* Has `url()` and `altText()` methods. + +#### `Subject` + +* Describes display information for a specific claim within a credential. +* Has `rawID()`, `valueType()`, `value()`, `rawValue()`, `isMasked()`, `hasOrder()`, `order()`, + `pattern()`, `attachment()` and `locale()` methods. +* Use the `localizedLabelsLength()` and `localizedLabelAtIndex()` methods to iterate over the different localized credential subject label. +* Display order data is optional and will only exist if the issuer provided it. Use the `hasOrder()` method + to determine if there is a specified order before attempting to retrieve the order, since `order()` will return an + error/throw an exception if the claim has no order information. If you've ensured that the claim has an order + (by using `hadOrder()`), then you can safely ignore the error/exception from the `order()` method. +* `IsMasked()` indicates whether this claim's value is masked. If this method returns true, then the `value()` method + will return the masked value while the `rawValue()` method will return the unmasked version. +* `rawValue()` returns the raw display value for this claim without any formatting. + For example, if this claim is masked, this method will return the unmasked version. + If no special formatting was applied to the display value, then this method will be equivalent to calling Value. +* `rawID()` returns the claim's ID, which is the raw field name (key) from the VC associated with this claim. + It's not localized or formatted for display. +* `valueType()` returns the value type for this claim - when it's "image", then you should expect the value data to be + formatted using the [data URL scheme](https://www.rfc-editor.org/rfc/rfc2397). For type=attachment, ignore the RawValue() and Value(), instead use Attachment() method. +* `attachment()` returns the attachment object for this claim. If the claim is not of type attachment. The object has `id()`, `type()`, `mimeType()`, `description()`, `URI()`, `hash()` and `hashAlg()` methods. + + ## Credential Status After a Verifiable Credential is issued, many credentials support the ability for the issuer to later _revoke_ the diff --git a/pkg/credentialschema/credentialdisplay.go b/pkg/credentialschema/credentialdisplay.go index 0eec94de..e747a7a9 100644 --- a/pkg/credentialschema/credentialdisplay.go +++ b/pkg/credentialschema/credentialdisplay.go @@ -411,3 +411,164 @@ func convertLogo(logo *issuer.Logo) *Logo { return nil } + +func buildCredentialDisplaysAllLocale( + vcs []*verifiable.Credential, + credentialConfigurationsSupported map[issuer.CredentialConfigurationID]*issuer.CredentialConfigurationSupported, + maskingString string, +) ([]Credential, error) { + var credentialDisplays []Credential + + for _, vc := range vcs { + // The call below creates a copy of the VC with the selective disclosures merged into the credential subject. + displayVC, err := vc.CreateDisplayCredential(verifiable.DisplayAllDisclosures()) + if err != nil { + return nil, err + } + + subject, err := getSubject(displayVC) // Will contain both selective and non-selective disclosures. + if err != nil { + return nil, err + } + + for _, credentialConfigurationSupported := range credentialConfigurationsSupported { + if !haveMatchingTypes(credentialConfigurationSupported, displayVC.Contents().Types) { + continue + } + + credentialDisplay, err := buildCredentialDisplayAllLocale(credentialConfigurationSupported, vc, subject, maskingString) + if err != nil { + return nil, err + } + + credentialDisplays = append(credentialDisplays, *credentialDisplay) + + break + } + } + + return credentialDisplays, nil +} + +func buildCredentialDisplayAllLocale( + credentialConfigurationSupported *issuer.CredentialConfigurationSupported, + vc *verifiable.Credential, + subject *verifiable.Subject, + maskingString string, +) (*Credential, error) { + resolvedClaims, err := resolveClaimsAllLocale(credentialConfigurationSupported, vc, subject, maskingString) + if err != nil { + return nil, err + } + + var overviews []CredentialOverview + for _, v := range credentialConfigurationSupported.LocalizedCredentialDisplays { + overviews = append(overviews, CredentialOverview{ + Name: v.Name, + Locale: v.Locale, + BackgroundColor: v.BackgroundColor, + TextColor: v.TextColor, + Logo: convertLogo(v.Logo), + }) + } + + return &Credential{LocalizedOverview: overviews, Subject: resolvedClaims}, nil +} + +func resolveClaimsAllLocale( + credentialConfigurationSupported *issuer.CredentialConfigurationSupported, + vc *verifiable.Credential, + credentialSubject *verifiable.Subject, + maskingString string, +) ([]Subject, error) { + var resolvedClaims []Subject + + for fieldName, claim := range credentialConfigurationSupported.CredentialDefinition.CredentialSubject { + resolvedClaim, err := resolveClaimAllLocale( + fieldName, claim, vc, credentialSubject, credentialConfigurationSupported, maskingString) + if err != nil && !errors.Is(err, errNoClaimDisplays) && !errors.Is(err, errClaimValueNotFoundInVC) { + return nil, err + } + + if resolvedClaim != nil { + resolvedClaims = append(resolvedClaims, *resolvedClaim) + } + } + + return resolvedClaims, nil +} + +func resolveClaimAllLocale( + fieldName string, + claim *issuer.Claim, + vc *verifiable.Credential, + credentialSubject *verifiable.Subject, + credentialConfigurationSupported *issuer.CredentialConfigurationSupported, + maskingString string, +) (*Subject, error) { + if len(claim.LocalizedClaimDisplays) == 0 { + return nil, errNoClaimDisplays + } + + var labels []Label + + for _, claimDisplay := range claim.LocalizedClaimDisplays { + labels = append(labels, Label{Name: claimDisplay.Name, Locale: claimDisplay.Locale}) + } + + untypedValue := getMatchingClaimValue(vc, credentialSubject, fieldName) + if untypedValue == nil { + return nil, errClaimValueNotFoundInVC + } + + var attachment *Attachment + + if claim.ValueType == "attachment" { + switch untypedValue.(type) { + case map[string]interface{}: + attachmentJSON, err := json.Marshal(untypedValue) + if err != nil { + fmt.Println("json marshal attachment ", err) + } + + attachment = &Attachment{} + if err := json.Unmarshal(attachmentJSON, &attachment); err != nil { + fmt.Println("json unmarshal attachment ", err) + } + default: + return nil, fmt.Errorf("unsupported attachment value '%v'", untypedValue) + } + } + + rawValue := fmt.Sprintf("%v", untypedValue) + + var value *string + + if claim.Mask != "" { + maskedValue, err := getMaskedValue(rawValue, claim.Mask, maskingString) + if err != nil { + return nil, err + } + + value = &maskedValue + } + + var order *int + + orderAsInt, err := credentialConfigurationSupported.ClaimOrderAsInt(fieldName) + if err == nil { + order = &orderAsInt + } + + return &Subject{ + RawID: fieldName, + LocalizedLabels: labels, + ValueType: claim.ValueType, + Order: order, + RawValue: rawValue, + Value: value, + Pattern: claim.Pattern, + Mask: claim.Mask, + Attachment: attachment, + }, nil +} diff --git a/pkg/credentialschema/credentialschema.go b/pkg/credentialschema/credentialschema.go index ffd47c17..d68b6ac5 100644 --- a/pkg/credentialschema/credentialschema.go +++ b/pkg/credentialschema/credentialschema.go @@ -38,6 +38,32 @@ func Resolve(opts ...ResolveOpt) (*ResolvedDisplayData, error) { }, nil } +// ResolveCredential resolves display information for some issued credentials based on an issuer's metadata. +func ResolveCredential(opts ...ResolveOpt) (*ResolvedData, error) { + vcs, metadata, _, maskingString, err := processOpts(opts) + if err != nil { + return nil, err + } + + if maskingString == nil { + defaultMaskingString := "•" + maskingString = &defaultMaskingString + } + + credentialDisplays, err := buildCredentialDisplaysAllLocale(vcs, metadata.CredentialConfigurationsSupported, + *maskingString) + if err != nil { + return nil, err + } + + issuerOverview := getIssuerDisplayAllLocale(metadata.LocalizedIssuerDisplays) + + return &ResolvedData{ + LocalizedIssuer: issuerOverview, + Credential: credentialDisplays, + }, nil +} + // ResolveCredentialOffer resolves display information for some offered credentials based on an issuer's metadata. // The CredentialDisplays in the returned ResolvedDisplayData object correspond to the offered credential types // passed in and are in the same order. diff --git a/pkg/credentialschema/credentialschema_test.go b/pkg/credentialschema/credentialschema_test.go index 58248610..eb7320db 100644 --- a/pkg/credentialschema/credentialschema_test.go +++ b/pkg/credentialschema/credentialschema_test.go @@ -485,6 +485,25 @@ func TestResolveCredentialOffer(t *testing.T) { }) } +func TestResolveCredential(t *testing.T) { //nolint: gocognit // Test file + credential, err := verifiable.ParseCredential(credentialUniversityDegree, + verifiable.WithCredDisableValidation(), + verifiable.WithDisabledProofCheck()) + require.NoError(t, err) + + var issuerMetadata issuer.Metadata + + err = json.Unmarshal(sampleIssuerMetadata, &issuerMetadata) + require.NoError(t, err) + + resolvedDisplayData, errResolve := credentialschema.ResolveCredential( + credentialschema.WithCredentials([]*verifiable.Credential{credential}), + credentialschema.WithIssuerMetadata(&issuerMetadata)) + require.NoError(t, errResolve) + + require.Equal(t, len(resolvedDisplayData.Credential), 1) +} + func checkSuccessCaseMatchedOverviewData(t *testing.T, resolvedDisplayData *credentialschema.ResolvedDisplayData) { t.Helper() diff --git a/pkg/credentialschema/issuerdisplay.go b/pkg/credentialschema/issuerdisplay.go index 2243a8fc..b45cb9fd 100644 --- a/pkg/credentialschema/issuerdisplay.go +++ b/pkg/credentialschema/issuerdisplay.go @@ -43,3 +43,20 @@ func getIssuerDisplay(issuerDisplays []issuer.LocalizedIssuerDisplay, locale str TextColor: issuerDisplays[0].TextColor, } } + +func getIssuerDisplayAllLocale(issuerDisplays []issuer.LocalizedIssuerDisplay) []ResolvedIssuerDisplay { + var resolvedIssuerDisplay []ResolvedIssuerDisplay + + for _, issuerDisplay := range issuerDisplays { + resolvedIssuerDisplay = append(resolvedIssuerDisplay, ResolvedIssuerDisplay{ + Name: issuerDisplay.Name, + Locale: issuerDisplay.Locale, + URL: issuerDisplay.URL, + Logo: convertLogo(issuerDisplay.Logo), + BackgroundColor: issuerDisplay.BackgroundColor, + TextColor: issuerDisplay.TextColor, + }) + } + + return resolvedIssuerDisplay +} diff --git a/pkg/credentialschema/models.go b/pkg/credentialschema/models.go index 595fe79c..7888dd32 100644 --- a/pkg/credentialschema/models.go +++ b/pkg/credentialschema/models.go @@ -56,6 +56,38 @@ type ResolvedClaim struct { Attachment *Attachment `json:"attachment,omitempty"` } +// ResolvedData represents display information for the credentials based on an issuer's metadata. +type ResolvedData struct { + LocalizedIssuer []ResolvedIssuerDisplay `json:"localized_issuer,omitempty"` + Credential []Credential `json:"credentials,omitempty"` +} + +// Credential represents display data for a credential. +// Display data for specific claims (e.g. first name, date of birth, etc.) are in Subject. +type Credential struct { + LocalizedOverview []CredentialOverview `json:"localized_overview,omitempty"` + Subject []Subject `json:"subjects,omitempty"` +} + +// Subject represents display data for a specific credential subject. +type Subject struct { + RawID string `json:"raw_id,omitempty"` + LocalizedLabels []Label `json:"localized_labels,omitempty"` + ValueType string `json:"value_type,omitempty"` + RawValue string `json:"raw_value,omitempty"` + Value *string `json:"value,omitempty"` + Order *int `json:"order,omitempty"` + Pattern string `json:"pattern,omitempty"` + Mask string `json:"mask,omitempty"` + Attachment *Attachment `json:"attachment,omitempty"` +} + +// Label represents display information for localizaed credential subject label. +type Label struct { + Name string `json:"name,omitempty"` + Locale string `json:"locale,omitempty"` +} + // Logo represents display information for a logo. type Logo struct { URL string `json:"uri,omitempty"`