@@ -6,13 +6,10 @@ import SwiftUI
66import WalletSDK
77
88private enum ProviderConfiguration {
9- static let appGroupIdentifier = " group.id.walt.wallet.demo "
10- static let documentTypesKey = " id.walt.wallet.identity-document-types "
11- static let registryIDKey = " id.walt.wallet.identity-document-registry-id "
12- static let registrationIDsKey = " id.walt.wallet.identity-document-registration-ids "
9+ static let appGroupIdentifier = IdentityDocumentSharedConfiguration . appGroupIdentifier
1310
1411 static var keychainAccessGroup : String {
15- guard let value = Bundle . main . object ( forInfoDictionaryKey : " WALTKeychainAccessGroup " ) as? String ,
12+ guard let value = IdentityDocumentSharedConfiguration . keychainAccessGroup ,
1613 !value. isEmpty else {
1714 preconditionFailure ( " The document provider requires a resolved shared Keychain access group " )
1815 }
@@ -38,31 +35,8 @@ struct WaltIdentityDocumentProvider: IdentityDocumentProvider {
3835 }
3936
4037 func performRegistrationUpdates( ) async {
41- guard let defaults = UserDefaults ( suiteName: ProviderConfiguration . appGroupIdentifier) else { return }
42- let store = IdentityDocumentProviderRegistrationStore ( )
43- guard await store. status == . authorized else { return }
44- do {
45- for identifier in defaults. stringArray ( forKey: ProviderConfiguration . registrationIDsKey) ?? [ ] {
46- try await store. removeRegistration ( forDocumentIdentifier: identifier)
47- }
48- let registryID = defaults. string ( forKey: ProviderConfiguration . registryIDKey) ?? " empty "
49- let documentTypes = ( defaults. stringArray ( forKey: ProviderConfiguration . documentTypesKey) ?? [ ] ) . sorted ( )
50- let identifiers = documentTypes. map {
51- " id.walt.wallet. \( registryID) . \( Data ( $0. utf8) . base64EncodedString ( ) ) "
52- }
53- for (documentType, identifier) in zip ( documentTypes, identifiers) {
54- try await store. addRegistration (
55- MobileDocumentRegistration (
56- mobileDocumentType: documentType,
57- supportedAuthorityKeyIdentifiers: [ ] ,
58- documentIdentifier: identifier
59- )
60- )
61- }
62- defaults. set ( identifiers, forKey: ProviderConfiguration . registrationIDsKey)
63- } catch {
64- // IdentityDocumentServices invokes this method again when registration state changes.
65- }
38+ // IdentityDocumentServices invokes this method again when registration state changes.
39+ try ? await IdentityDocumentRegistrationCoordinator . update ( )
6640 }
6741}
6842
@@ -73,6 +47,7 @@ private struct AnnexCConsentView: View {
7347 @State private var wallet : Wallet ?
7448 @State private var failure : String ?
7549 @State private var isSubmitting = false
50+ @State private var selectedCredentialIDsByQuery : [ String : String ] = [ : ]
7651
7752 var body : some View {
7853 NavigationStack {
@@ -85,7 +60,7 @@ private struct AnnexCConsentView: View {
8560 Text ( preview. verifiedOrigin)
8661 }
8762 Section ( " Requested information " ) {
88- ForEach ( preview. parsedRequest. documents, id: \. documentType ) { document in
63+ ForEach ( Array ( preview. parsedRequest. documents. enumerated ( ) ) , id: \. offset ) { _ , document in
8964 VStack ( alignment: . leading, spacing: 4 ) {
9065 Text ( document. documentType) . font ( . headline)
9166 Text ( document. namespaces. values. flatMap { $0 } . sorted ( ) . joined ( separator: " , " ) )
@@ -94,6 +69,29 @@ private struct AnnexCConsentView: View {
9469 }
9570 }
9671 }
72+ Section ( " Credentials " ) {
73+ ForEach ( queryIDs ( in: preview) , id: \. self) { queryID in
74+ let options = credentialOptions ( for: queryID, in: preview)
75+ Picker (
76+ " Credential " ,
77+ selection: Binding < String ? > (
78+ get: { selectedCredentialIDsByQuery [ queryID] } ,
79+ set: { selectedCredentialIDsByQuery [ queryID] = $0 }
80+ )
81+ ) {
82+ Text ( " Choose a credential " ) . tag ( nil as String ? )
83+ ForEach ( options) { option in
84+ Text ( credentialTitle ( option) ) . tag ( option. credentialID as String ? )
85+ }
86+ }
87+ if let credentialID = selectedCredentialIDsByQuery [ queryID] ,
88+ let selected = options. first ( where: { $0. credentialID == credentialID } ) {
89+ Text ( credentialDetail ( selected) )
90+ . font ( . caption)
91+ . foregroundStyle ( . secondary)
92+ }
93+ }
94+ }
9795 Section ( " Reader trust " ) {
9896 Text ( readerTrustDescription ( preview. readerTrust) )
9997 }
@@ -109,7 +107,7 @@ private struct AnnexCConsentView: View {
109107 }
110108 ToolbarItem ( placement: . confirmationAction) {
111109 Button ( " Share " ) { submit ( ) }
112- . disabled ( preview == nil || isSubmitting)
110+ . disabled ( preview == nil || !hasCompleteSelection || isSubmitting)
113111 }
114112 }
115113 . task { await prepare ( ) }
@@ -126,26 +124,40 @@ private struct AnnexCConsentView: View {
126124 let wallet = try await Wallet ( configuration: ProviderConfiguration . walletConfiguration)
127125 _ = try await wallet. bootstrap ( )
128126 self . wallet = wallet
129- preview = try await wallet. previewAnnexCPresentation (
127+ let preparedPreview = try await wallet. previewAnnexCPresentation (
130128 parsedRequest: parsed,
131129 verifiedOrigin: try canonicalOrigin ( origin)
132130 )
131+ preview = preparedPreview
132+ selectedCredentialIDsByQuery = Dictionary (
133+ uniqueKeysWithValues: queryIDs ( in: preparedPreview) . compactMap { queryID in
134+ let options = credentialOptions ( for: queryID, in: preparedPreview)
135+ return options. count == 1 ? ( queryID, options [ 0 ] . credentialID) : nil
136+ }
137+ )
133138 } catch {
134139 failure = error. localizedDescription
135140 }
136141 }
137142
138143 private func submit( ) {
139144 guard let wallet, let preview else { return }
145+ let selections = queryIDs ( in: preview) . compactMap { queryID -> PresentationCredentialSelection ? in
146+ guard let credentialID = selectedCredentialIDsByQuery [ queryID] ,
147+ let option = credentialOptions ( for: queryID, in: preview) . first ( where: {
148+ $0. credentialID == credentialID
149+ } ) else { return nil }
150+ return option. selection
151+ }
152+ guard selections. count == queryIDs ( in: preview) . count else {
153+ failure = ProviderFailure . missingCredentialSelection. localizedDescription
154+ return
155+ }
140156 isSubmitting = true
141157 Task {
142158 do {
143159 try await context. sendResponse { rawRequest in
144160 let raw = try RawAnnexCRequest ( data: rawRequest. requestData)
145- let selections = preview. credentialOptions. reduce ( into: [ PresentationCredentialSelection] ( ) ) { result, option in
146- guard !result. contains ( where: { $0. queryID == option. queryID } ) else { return }
147- result. append ( option. selection)
148- }
149161 let response = try await wallet. submitAnnexCPresentation (
150162 requestID: preview. requestID,
151163 verifiedOrigin: preview. verifiedOrigin,
@@ -169,6 +181,35 @@ private struct AnnexCConsentView: View {
169181 }
170182 }
171183
184+ private var hasCompleteSelection : Bool {
185+ guard let preview else { return false }
186+ return queryIDs ( in: preview) . allSatisfy { queryID in
187+ guard let credentialID = selectedCredentialIDsByQuery [ queryID] else { return false }
188+ return credentialOptions ( for: queryID, in: preview) . contains { $0. credentialID == credentialID }
189+ }
190+ }
191+
192+ private func queryIDs( in preview: AnnexCPresentationPreview ) -> [ String ] {
193+ Array ( Set ( preview. credentialOptions. map ( \. queryID) ) ) . sorted ( )
194+ }
195+
196+ private func credentialOptions(
197+ for queryID: String ,
198+ in preview: AnnexCPresentationPreview
199+ ) -> [ PresentationCredentialOption ] {
200+ preview. credentialOptions. filter { $0. queryID == queryID }
201+ }
202+
203+ private func credentialTitle( _ option: PresentationCredentialOption ) -> String {
204+ option. label ?? option. issuer ?? " Credential "
205+ }
206+
207+ private func credentialDetail( _ option: PresentationCredentialOption ) -> String {
208+ [ option. issuer, option. subject, option. credentialID]
209+ . compactMap { $0 }
210+ . joined ( separator: " · " )
211+ }
212+
172213 private func parse( _ request: ISO18013MobileDocumentRequest ) throws -> AnnexCParsedRequest {
173214 var documents : [ AnnexCDocumentRequest ] = [ ]
174215 for presentment in request. presentmentRequests {
@@ -238,13 +279,15 @@ private enum ProviderFailure: LocalizedError {
238279 case alternativeRequestSetsUnsupported
239280 case emptyRequest
240281 case invalidResponseEncoding
282+ case missingCredentialSelection
241283 case missingVerifiedOrigin
242284
243285 var errorDescription : String ? {
244286 switch self {
245287 case . alternativeRequestSetsUnsupported: return " Alternative document request sets are not supported "
246288 case . emptyRequest: return " The request does not contain any documents "
247289 case . invalidResponseEncoding: return " The encrypted response could not be encoded "
290+ case . missingCredentialSelection: return " Choose one credential for every requested document "
248291 case . missingVerifiedOrigin: return " IdentityDocumentServices did not assert a website origin "
249292 }
250293 }
0 commit comments