From 3ab2897800136e1caca81f8232b9474d00cace1d Mon Sep 17 00:00:00 2001 From: Avner Matan <137777701+avner-m@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:00:31 +1200 Subject: [PATCH 1/2] Update iOS and Android holder sample apps for new Holder SDK release Align the iOS and Android holder tutorial sample apps with the iOS Holder SDK v6.0.0 and Android Holder SDK v7.0.0 APIs, matching the updated tutorials in MATTR Learn. iOS (v6.0.0): - Migrate ViewModel from ObservableObject/@Published to @Observable/@State - Handle async initialize() via a .task modifier - Switch RetrieveCredentialResult to .success/.failure enum handling - Use OnlinePresentationSession.getMatchedCredentials() - Drop optional chaining on non-optional MobileCredential(Metadata).claims - Remove unused Combine imports Android (v7.0.0): - Bump holder SDK dependency to 7.0.0 - Handle RetrieveCredentialResult sealed interface (Success/Failure) - Rename OfferedCredential.doctype to docType - Use OnlinePresentationSession.getMatchedCredentials() --- .../app/build.gradle.kts | 2 +- .../example/holdertutorial/MainActivity.kt | 33 +++++---- .../OnlinePresentationScreen.kt | 4 +- .../iOS Holder Tutorial/ContentView.swift | 67 +++++++++++-------- .../iOS Holder Tutorial/DocumentView.swift | 9 ++- .../PresentCredentialsView.swift | 9 +-- .../TransactionCodeInputView.swift | 2 +- 7 files changed, 70 insertions(+), 56 deletions(-) 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..3431ab9 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() + } } } @@ -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 From 8267dcc780646698e9b9567491571f0197e4d3ca Mon Sep 17 00:00:00 2001 From: Avner Matan <137777701+avner-m@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:05:34 +1200 Subject: [PATCH 2/2] Handle optional OfferedCredential.claims in iOS holder sample OfferedCredential.claims is now optional in iOS Holder SDK v6.0.0; guard the claim count shown for a discovered credential offer. --- .../iOS Holder Tutorial/ContentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3431ab9..aa93336 100644 --- a/ios-holder-tutorial-sample-app/iOS Holder Tutorial/ContentView.swift +++ b/ios-holder-tutorial-sample-app/iOS Holder Tutorial/ContentView.swift @@ -115,7 +115,7 @@ struct ContentView: View { Text("No. of claims:") .bold() Spacer() - Text("\(credential.claims.count)") + Text("\(credential.claims?.count ?? 0)") } } }