diff --git a/android-holder-tutorial-sample-app/app/build.gradle.kts b/android-holder-tutorial-sample-app/app/build.gradle.kts index 28d7a74..e0ee922 100644 --- a/android-holder-tutorial-sample-app/app/build.gradle.kts +++ b/android-holder-tutorial-sample-app/app/build.gradle.kts @@ -56,7 +56,7 @@ dependencies { androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) - implementation("global.mattr.mobilecredential:holder:6.1.2") + implementation("global.mattr.mobilecredential:holder:7.0.0") implementation("androidx.navigation:navigation-compose:2.9.0") implementation("com.google.accompanist:accompanist-permissions:0.36.0") implementation("com.journeyapps:zxing-android-embedded:4.3.0") diff --git a/android-holder-tutorial-sample-app/app/src/main/java/com/example/holdertutorial/MainActivity.kt b/android-holder-tutorial-sample-app/app/src/main/java/com/example/holdertutorial/MainActivity.kt index ac71b80..09deb47 100644 --- a/android-holder-tutorial-sample-app/app/src/main/java/com/example/holdertutorial/MainActivity.kt +++ b/android-holder-tutorial-sample-app/app/src/main/java/com/example/holdertutorial/MainActivity.kt @@ -41,6 +41,7 @@ import global.mattr.mobilecredential.holder.MobileCredentialHolder import global.mattr.mobilecredential.holder.ProximityPresentationSession import global.mattr.mobilecredential.holder.issuance.CredentialIssuanceConfiguration import global.mattr.mobilecredential.holder.issuance.dto.DiscoveredCredentialOffer +import global.mattr.mobilecredential.holder.issuance.dto.RetrieveCredentialResult import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -142,11 +143,11 @@ fun HomeScreen(activity: Activity, navController: NavController) { SharedData.discoveredCredentialOffer?.let { discoveredOffer -> Text("Received Credential Offer from ${discoveredOffer.issuer}") LazyColumn(Modifier.fillMaxWidth()) { - items(discoveredOffer.credentials, key = { it.doctype }) { credential -> + items(discoveredOffer.credentials, key = { it.docType }) { credential -> Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(4.dp)) { Text("Name: ${credential.name ?: ""}") - Text("DocType: ${credential.doctype}") + Text("DocType: ${credential.docType}") } } } @@ -196,16 +197,24 @@ private fun onRetrieveCredentials( ) // Claim Credential - Step 4.6: Display retrieved credentials - SharedData.retrievedCredentials = retrieveCredentialResults.mapNotNull { - try { - // The credential ID can be used to get the full credential from the SDK storage. - // fetchUpdatedStatusList - Whether to enforce the online revocation status check for the credential. - // Returned object contains all credential data, including the user's PII. - mdocHolder.getCredential(it.credentialId!!, fetchUpdatedStatusList = false) - } catch (e: Exception) { - val msg = "Failed to get credential from storage" - Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show() - null + // RetrieveCredentialResult is a sealed interface with Success and Failure variants. + SharedData.retrievedCredentials = retrieveCredentialResults.mapNotNull { result -> + when (result) { + is RetrieveCredentialResult.Success -> try { + // The credential ID can be used to get the full credential from the SDK storage. + // fetchUpdatedStatusList - Whether to enforce the online revocation status check for the credential. + // Returned object contains all credential data, including the user's PII. + mdocHolder.getCredential(result.credentialId, fetchUpdatedStatusList = false) + } catch (e: Exception) { + val msg = "Failed to get credential from storage" + Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show() + null + } + is RetrieveCredentialResult.Failure -> { + val msg = "Failed to retrieve ${result.docType}: ${result.error}" + Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show() + null + } } } diff --git a/android-holder-tutorial-sample-app/app/src/main/java/com/example/holdertutorial/OnlinePresentationScreen.kt b/android-holder-tutorial-sample-app/app/src/main/java/com/example/holdertutorial/OnlinePresentationScreen.kt index 2769a48..2408545 100644 --- a/android-holder-tutorial-sample-app/app/src/main/java/com/example/holdertutorial/OnlinePresentationScreen.kt +++ b/android-holder-tutorial-sample-app/app/src/main/java/com/example/holdertutorial/OnlinePresentationScreen.kt @@ -57,10 +57,10 @@ fun OnlinePresentationScreen(activity: Activity, requestUri: String) { } } - // session.matchedCredentials - Map that pairs credential requests to lists of the stored credentials that match those requests. + // session.getMatchedCredentials() - Map that pairs credential requests to lists of the stored credentials that match those requests. // Credentials in the list will only contain the requested claim names (e.g. "given_name", "family_name") without values. // Values can be retrieved from storage by calling getCredential() with the credential id. - val (requested, matched) = session?.matchedCredentials?.entries?.firstOrNull() ?: return + val (requested, matched) = session?.getMatchedCredentials()?.entries?.firstOrNull() ?: return var matchedCredentials by remember { mutableStateOf(matched) } var selectedCredentialId by remember { mutableStateOf(matchedCredentials.first().id) } diff --git a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/ContentView.swift b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/ContentView.swift index 9045649..aa93336 100644 --- a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/ContentView.swift +++ b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/ContentView.swift @@ -4,12 +4,11 @@ // import SwiftUI -import Combine // Claim Credential - Step 1.2: Import MobileCredentialHolderSDK import MobileCredentialHolderSDK struct ContentView: View { - @ObservedObject var viewModel: ViewModel = ViewModel() + @State var viewModel: ViewModel = ViewModel() var body: some View { NavigationStack(path: $viewModel.navigationPath) { VStack { @@ -73,6 +72,10 @@ struct ContentView: View { // Navigate to online presentation view viewModel.navigationPath.append(NavigationState.onlinePresentation) } + // Claim Credential - Step 1.4: Initialize the SDK when the view appears + .task { + await viewModel.initialize() + } } } @@ -112,7 +115,7 @@ struct ContentView: View { Text("No. of claims:") .bold() Spacer() - Text("\(credential.claims.count)") + Text("\(credential.claims?.count ?? 0)") } } } @@ -191,32 +194,32 @@ struct ContentView: View { } } -class ViewModel: ObservableObject { - @Published var navigationPath = NavigationPath() +@Observable class ViewModel { + var navigationPath = NavigationPath() // Claim Credential - Step 1.3: Add MobileCredentialHolder var var mobileCredentialHolder: MobileCredentialHolder // Claim Credential - Step 3.1: Add DiscoveredCredentialOffer and discoveredCredentialOfferURL vars - @Published var discoveredCredentialOffer: DiscoveredCredentialOffer? + var discoveredCredentialOffer: DiscoveredCredentialOffer? var discoveredCredentialOfferURL = "" // Claim Credential - Step 4.1: Add retrievedCredentials var - @Published var retrievedCredentials: [MobileCredential] = [] + var retrievedCredentials: [MobileCredential] = [] // Proximity Presentation - Step 1.2: Create deviceEngagementString and proximityPresentationSession variables - @Published var deviceEngagementString: String? - @Published var proximityPresentationSession: ProximityPresentationSession? + var deviceEngagementString: String? + var proximityPresentationSession: ProximityPresentationSession? // Proximity and Online Presentation: Create variables for credential presentations - @Published var matchedCredentials: [MobileCredential] = [] - @Published var matchedMetadata: [MobileCredentialMetadata] = [] - @Published var credentialRequest: [MobileCredentialRequest] = [] + var matchedCredentials: [MobileCredential] = [] + var matchedMetadata: [MobileCredentialMetadata] = [] + var credentialRequest: [MobileCredentialRequest] = [] // Online Presentation - Step 2.1: Create a variable to hold the online presentation session object - @Published var onlinePresentationSession: OnlinePresentationSession? + var onlinePresentationSession: OnlinePresentationSession? var shouldDisplayOnlinePresentation: Bool { @@ -226,18 +229,24 @@ class ViewModel: ObservableObject { // Claim Credential - Step 1.4: Initialize MobileCredentialHolder SDK init() { - do { mobileCredentialHolder = MobileCredentialHolder.shared - try mobileCredentialHolder.initialize( - userAuthenticationConfiguration: UserAuthenticationConfiguration(userAuthenticationBehavior: .onDeviceKeyAccess), - credentialIssuanceConfiguration: CredentialIssuanceConfiguration( - redirectUri: Constants.redirectUri, - autoTrustMobileCredentialIaca: true - ) - ) - } catch { - print(error) } + + // `initialize` is asynchronous as of iOS Holder SDK v6.0.0, so it runs in an async method called + // from the view's `.task` modifier when the view appears. + @MainActor + func initialize() async { + do { + try await mobileCredentialHolder.initialize( + userAuthenticationConfiguration: UserAuthenticationConfiguration(userAuthenticationBehavior: .onDeviceKeyAccess), + credentialIssuanceConfiguration: CredentialIssuanceConfiguration( + redirectUri: Constants.redirectUri, + autoTrustMobileCredentialIaca: true + ) + ) + } catch { + print(error) + } } @MainActor @@ -286,10 +295,13 @@ extension ViewModel { Task { var credentials: [MobileCredential] = [] for result in retrievedCredentialResults { - if let credentialId = result.credentialId { + switch result { + case .success(_, let credentialId): if let credential = try? await mobileCredentialHolder.getCredential(credentialId: credentialId) { credentials.append(credential) } + case .failure(let docType, let error): + print("Failed to retrieve \(docType): \(error)") } } self.retrievedCredentials = credentials @@ -313,13 +325,12 @@ extension ViewModel { Task { do { onlinePresentationSession = try await mobileCredentialHolder.createOnlinePresentationSession(authorizationRequestUri: authorizationRequestURI, requireTrustedVerifier: false) - matchedMetadata = onlinePresentationSession?.matchedCredentials? + let matched = onlinePresentationSession?.getMatchedCredentials() ?? [] + matchedMetadata = matched .flatMap { $0.matchedMobileCredentials } - .compactMap { $0 } ?? [] - credentialRequest = onlinePresentationSession?.matchedCredentials? - .compactMap { $0.request } - .compactMap { $0 } ?? [] + credentialRequest = matched + .map { $0.request } } catch { print(error.localizedDescription) } diff --git a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/DocumentView.swift b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/DocumentView.swift index edf6282..b573929 100644 --- a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/DocumentView.swift +++ b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/DocumentView.swift @@ -5,7 +5,6 @@ import MobileCredentialHolderSDK import SwiftUI -import Combine struct DocumentView: View { @@ -53,23 +52,23 @@ import Combine // MARK: DocumentViewModel - class DocumentViewModel: ObservableObject { + class DocumentViewModel { var docType: String var namespacesAndClaims: [String: [String: String?]] init(from credential: MobileCredential) { self.docType = credential.docType - self.namespacesAndClaims = credential.claims?.reduce(into: [String: [String: String]]()) { result, outerElement in + self.namespacesAndClaims = credential.claims.reduce(into: [String: [String: String]]()) { result, outerElement in let (outerKey, innerDict) = outerElement result[outerKey] = innerDict.mapValues { $0.textRepresentation } - } ?? [:] + } } init(from credentialMetadata: MobileCredentialMetadata) { self.docType = credentialMetadata.docType var result: [String: [String: String?]] = [:] - credentialMetadata.claims?.forEach { namespace, claimIDs in + credentialMetadata.claims.forEach { namespace, claimIDs in var transformedClaims: [String: String?] = [:] claimIDs.forEach { claimID in transformedClaims[claimID] = Optional.none diff --git a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/PresentCredentialsView.swift b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/PresentCredentialsView.swift index 6d51cf1..1ffe340 100644 --- a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/PresentCredentialsView.swift +++ b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/PresentCredentialsView.swift @@ -6,16 +6,11 @@ import MobileCredentialHolderSDK import SwiftUI -import Combine struct PresentCredentialsView: View { - @ObservedObject var viewModel: PresentCredentialsViewModel + var viewModel: PresentCredentialsViewModel @State var selectedID: String? - init(viewModel: PresentCredentialsViewModel) { - self.viewModel = viewModel - } - var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { @@ -81,7 +76,7 @@ struct PresentCredentialsView: View { // MARK: PresentCredentialsViewModel -class PresentCredentialsViewModel: ObservableObject { +class PresentCredentialsViewModel { @Binding var requestedDocuments: [MobileCredentialRequest] @Binding var matchedCredentials: [MobileCredential] @Binding var matchedMetadata: [MobileCredentialMetadata] diff --git a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/TransactionCodeInputView.swift b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/TransactionCodeInputView.swift index ab15955..14772c6 100644 --- a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/TransactionCodeInputView.swift +++ b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/TransactionCodeInputView.swift @@ -6,7 +6,7 @@ import SwiftUI struct TransactionCodeInputView: View { - @ObservedObject var viewModel: ViewModel + var viewModel: ViewModel @State private var transactionCode = "" @Environment(\.dismiss) private var dismiss