From 4bf3c665b1ed7148a61866e1ad62ac92f5d81456 Mon Sep 17 00:00:00 2001 From: Avner Matan <137777701+avner-m@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:00:42 +1200 Subject: [PATCH 1/2] feat(verifier): require SDK Tethering for iOS/Android verifier sample apps Update the in-person and remote verification sample apps for the Verifier SDK breaking changes (iOS Verifier 6.0.0, Android 7.0.0). React Native and web verifier apps are out of scope for this change. In-person (proximity): - android: initialize with a PlatformConfiguration (tenantHost + applicationId), handle FailedToRegister/InvalidLicense, bump verifier dependency to 7.0.0, treat MobileCredentialResponse.credentials/.credentialErrors as non-null - ios: async initialize with PlatformConfiguration + tethering error handling, add tenantHost/applicationId constants, rename verificationResult.reason to .failureType Remote (app-to-app / OID4VP): - android: supply applicationId via PlatformConfiguration (removed from requestMobileCredentials), handle the new sealed OnlinePresentationSessionResult (Success/Failure), tethering error handling, bump verifier dependency to 7.0.0 - ios: applicationId via PlatformConfiguration (removed from requestMobileCredentials), async initialize, rename .reason to .failureType --- .../app/build.gradle.kts | 2 +- .../example/verifiertutorial/MainActivity.kt | 29 +++++++++--- .../verifiertutorial/ViewResponseScreen.kt | 4 +- .../app/build.gradle.kts | 2 +- .../mobileverifiertutorial/MainActivity.kt | 46 ++++++++++++++----- .../Constants.swift | 9 +++- .../ContentView.swift | 25 +++++++--- .../DocumentView.swift | 3 +- .../ContentView.swift | 30 ++++++++---- .../DocumentView.swift | 3 +- 10 files changed, 114 insertions(+), 39 deletions(-) diff --git a/android-in-person-verifier-tutorial-sample-app/app/build.gradle.kts b/android-in-person-verifier-tutorial-sample-app/app/build.gradle.kts index 4cd523e..cb1e13f 100644 --- a/android-in-person-verifier-tutorial-sample-app/app/build.gradle.kts +++ b/android-in-person-verifier-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:verifier:6.1.0") + implementation("global.mattr.mobilecredential:verifier: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-in-person-verifier-tutorial-sample-app/app/src/main/java/com/example/verifiertutorial/MainActivity.kt b/android-in-person-verifier-tutorial-sample-app/app/src/main/java/com/example/verifiertutorial/MainActivity.kt index 079c102..f100586 100644 --- a/android-in-person-verifier-tutorial-sample-app/app/src/main/java/com/example/verifiertutorial/MainActivity.kt +++ b/android-in-person-verifier-tutorial-sample-app/app/src/main/java/com/example/verifiertutorial/MainActivity.kt @@ -21,7 +21,11 @@ import androidx.navigation.compose.rememberNavController import com.example.verifiertutorial.ui.theme.VerifierTutorialTheme import global.mattr.mobilecredential.verifier.dto.MobileCredentialResponse import global.mattr.mobilecredential.verifier.MobileCredentialVerifier +import global.mattr.mobilecredential.common.platformconfig.PlatformConfiguration +import global.mattr.mobilecredential.verifier.exception.VerifierException.FailedToRegisterException +import global.mattr.mobilecredential.verifier.exception.VerifierException.InvalidLicenseException import kotlinx.coroutines.launch +import java.net.URL class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -30,12 +34,25 @@ class MainActivity : ComponentActivity() { // The SDK must be initialized before any other SDK functions are called. // Initialize SDK - Step 1.1: Initialize the SDK lifecycleScope.launch { - // This function initializes storage and performs validations to ensure the SDK is ready for use. - // Only the application context is persisted. - MobileCredentialVerifier.initialize(this@MainActivity) - // Setup certificates - Step 2.1: Add trusted issuer certificates - // The added certificates will be the trust chain roots used to verify signatures of the received credentials. - MobileCredentialVerifier.addTrustedIssuerCertificates(listOf(Iacas.mattrLabs)) + // From Android Verifier SDK v7.0.0, the SDK is tethered to a MATTR VII tenant. Initialization + // registers this app instance with the tenant and obtains a license, so a PlatformConfiguration + // pointing at your tenant and Verifier Application is required. + val platformConfiguration = PlatformConfiguration( + tenantHost = URL("https://your-tenant.vii.mattr.global"), + applicationId = "" + ) + try { + // This function initializes storage, registers the app instance, obtains a license, + // and performs validations to ensure the SDK is ready for use. + MobileCredentialVerifier.initialize(this@MainActivity, platformConfiguration) + // Setup certificates - Step 2.1: Add trusted issuer certificates + // The added certificates will be the trust chain roots used to verify signatures of the received credentials. + MobileCredentialVerifier.addTrustedIssuerCertificates(listOf(Iacas.mattrLabs)) + } catch (e: FailedToRegisterException) { + // Registration with the MATTR VII tenant failed — check connectivity and configuration. + } catch (e: InvalidLicenseException) { + // The SDK license is missing, invalid, or expired. + } } enableEdgeToEdge() diff --git a/android-in-person-verifier-tutorial-sample-app/app/src/main/java/com/example/verifiertutorial/ViewResponseScreen.kt b/android-in-person-verifier-tutorial-sample-app/app/src/main/java/com/example/verifiertutorial/ViewResponseScreen.kt index 8e15c75..9d390d9 100644 --- a/android-in-person-verifier-tutorial-sample-app/app/src/main/java/com/example/verifiertutorial/ViewResponseScreen.kt +++ b/android-in-person-verifier-tutorial-sample-app/app/src/main/java/com/example/verifiertutorial/ViewResponseScreen.kt @@ -32,8 +32,10 @@ import global.mattr.mobilecredential.verifier.dto.MobileCredentialElement fun ViewResponseScreen() { // Verify mDocs - Step 4.5: Define content // For tutorial simplicity, only the first credential in the response is displayed, since the sample request only asks for a single document type (mDL). + // From Android Verifier SDK v7.0.0, MobileCredentialResponse.credentials and .credentialErrors + // are non-nullable lists, so we check for emptiness rather than null. val credential = SharedData.credentialResponse?.credentials?.firstOrNull() - if (credential == null || SharedData.credentialResponse?.credentialErrors != null) { + if (credential == null || SharedData.credentialResponse?.credentialErrors?.isNotEmpty() == true) { // Verify mDocs - Step 4.6: Show error // Show an error if something went wrong during the retrieval Box(Modifier.fillMaxSize()) { diff --git a/android-remote-verification-tutorial-sample-app/app/build.gradle.kts b/android-remote-verification-tutorial-sample-app/app/build.gradle.kts index e5bd1d5..e7471f2 100644 --- a/android-remote-verification-tutorial-sample-app/app/build.gradle.kts +++ b/android-remote-verification-tutorial-sample-app/app/build.gradle.kts @@ -62,6 +62,6 @@ dependencies { debugImplementation(libs.androidx.compose.ui.test.manifest) // MATTR MobileCredentialVerifier SDK — resolved from the local `repo` directory. - implementation("global.mattr.mobilecredential:verifier:6.1.0") + implementation("global.mattr.mobilecredential:verifier:7.0.0") implementation("androidx.navigation:navigation-compose:2.9.0") } diff --git a/android-remote-verification-tutorial-sample-app/app/src/main/java/com/example/mobileverifiertutorial/MainActivity.kt b/android-remote-verification-tutorial-sample-app/app/src/main/java/com/example/mobileverifiertutorial/MainActivity.kt index a176b2f..07fa7fc 100644 --- a/android-remote-verification-tutorial-sample-app/app/src/main/java/com/example/mobileverifiertutorial/MainActivity.kt +++ b/android-remote-verification-tutorial-sample-app/app/src/main/java/com/example/mobileverifiertutorial/MainActivity.kt @@ -28,11 +28,14 @@ import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.compose.viewModel import com.example.mobileverifiertutorial.ui.theme.MobileVerifierTutorialTheme import global.mattr.mobilecredential.verifier.MobileCredentialVerifier -import global.mattr.mobilecredential.verifier.PlatformConfiguration +import global.mattr.mobilecredential.verifier.OnlinePresentationSessionResult +import global.mattr.mobilecredential.common.platformconfig.PlatformConfiguration import global.mattr.mobilecredential.verifier.deviceretrieval.devicerequest.DataElements import global.mattr.mobilecredential.verifier.deviceretrieval.devicerequest.NameSpaces import global.mattr.mobilecredential.verifier.dto.MobileCredentialPresentation import global.mattr.mobilecredential.verifier.dto.MobileCredentialRequest +import global.mattr.mobilecredential.verifier.exception.VerifierException.FailedToRegisterException +import global.mattr.mobilecredential.verifier.exception.VerifierException.InvalidLicenseException import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -57,16 +60,27 @@ class MainActivity : ComponentActivity() { } } - // Step 2.3: Setup platform configuration with the MATTR VII tenant host. + // Step 2.3: Setup platform configuration with the MATTR VII tenant host and Verifier + // Application id. From Android Verifier SDK v7.0.0, the applicationId is supplied here (it is + // no longer passed to requestMobileCredentials) and also drives SDK Tethering. val platformConfiguration = PlatformConfiguration( - tenantHost = URL(Constants.TENANT_HOST) + tenantHost = URL(Constants.TENANT_HOST), + applicationId = Constants.APPLICATION_ID ) - // Step 2.4: Initialize the SDK. Must complete before any other SDK calls. + // Step 2.4: Initialize the SDK. Must complete before any other SDK calls. Initialization + // registers the app instance with the tenant and obtains a license, so it can throw + // FailedToRegisterException and InvalidLicenseException. lifecycleScope.launch { - MobileCredentialVerifier.initialize( - context = this@MainActivity, platformConfiguration = platformConfiguration - ) + try { + MobileCredentialVerifier.initialize( + context = this@MainActivity, platformConfiguration = platformConfiguration + ) + } catch (e: FailedToRegisterException) { + // Registration with the MATTR VII tenant failed — check connectivity and configuration. + } catch (e: InvalidLicenseException) { + // The SDK license is missing, invalid, or expired. + } } } } @@ -135,15 +149,23 @@ class VerifierViewModel : ViewModel() { // Step 3.2: Request credentials. The SDK launches the wallet via OID4VP and // suspends until the wallet redirects back via the deep link registered in // AndroidManifest.xml, at which point the response is returned here. + // From v7.0.0, applicationId is no longer passed here — it comes from the + // PlatformConfiguration supplied at initialization. val onlinePresentationResult = MobileCredentialVerifier.requestMobileCredentials( activity = activity, - request = listOf(mobileCredentialRequest), - applicationId = Constants.APPLICATION_ID + request = listOf(mobileCredentialRequest) ) - // Step 4.2: Surface the verified credentials to the UI. - _receivedDocuments.value = - onlinePresentationResult.mobileCredentialResponse?.credentials ?: emptyList() + // Step 4.2: Surface the verified credentials to the UI. From v7.0.0, + // OnlinePresentationSessionResult is a sealed interface with Success/Failure variants. + _receivedDocuments.value = when (onlinePresentationResult) { + is OnlinePresentationSessionResult.Success -> + onlinePresentationResult.mobileCredentialResponse?.credentials ?: emptyList() + is OnlinePresentationSessionResult.Failure -> { + // onlinePresentationResult.error is available here. + emptyList() + } + } } catch (e: Exception) { e.printStackTrace() } diff --git a/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/Constants.swift b/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/Constants.swift index 58f9c32..edfebcd 100644 --- a/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/Constants.swift +++ b/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/Constants.swift @@ -6,8 +6,15 @@ // Certificate Management screen. // // In a production app, you would manage these certificates dynamically rather than hardcoding them. +import Foundation + enum Constants { - static let montcliffPEM = + // From iOS Verifier SDK v6.0.0, the SDK is tethered to a MATTR VII tenant. Replace these with + // your tenant host and the Verifier Application `id` created on that tenant. + static let tenantHost = URL(string: "https://your-tenant.vii.mattr.global")! + static let applicationId = "" + + static let montcliffPEM = """ MIICYzCCAgmgAwIBAgIKXhjLoCkLWBxREDAKBggqhkjOPQQDAjA4MQswCQYDVQQG EwJBVTEpMCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0Ew diff --git a/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/ContentView.swift b/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/ContentView.swift index 997a859..5772f4d 100644 --- a/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/ContentView.swift +++ b/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/ContentView.swift @@ -138,14 +138,25 @@ final class VerifierViewModel { // to prepare the SDK for use. A trusted IACA certificate is also pre-loaded so the app // can verify mDocs issued by the Montcliff DMV test issuer out of the box. init() { - do { - mobileCredentialVerifier = MobileCredentialVerifier.shared - try mobileCredentialVerifier.initialize() - Task { - try? await mobileCredentialVerifier.addTrustedIssuerCertificates(certificates: [Constants.montcliffPEM]) + mobileCredentialVerifier = MobileCredentialVerifier.shared + // From v6.0.0, initialize is asynchronous and requires a PlatformConfiguration. On first + // launch it registers this app instance with the tenant and obtains a license, so it can + // throw failedToRegister and invalidLicense. + Task { + do { + let platformConfiguration = PlatformConfiguration( + tenantHost: Constants.tenantHost, + applicationId: Constants.applicationId + ) + try await mobileCredentialVerifier.initialize(platformConfiguration: platformConfiguration) + try? await mobileCredentialVerifier.addTrustedIssuerCertificates(certificates: [Constants.montcliffPEM]) + } catch MobileCredentialVerifierError.failedToRegister { + // Registration with the MATTR VII tenant failed — check connectivity and configuration. + } catch MobileCredentialVerifierError.invalidLicense { + // The SDK license is missing, invalid, or expired. + } catch { + print(error.localizedDescription) } - } catch { - print(error.localizedDescription) } } } diff --git a/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/DocumentView.swift b/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/DocumentView.swift index c388c84..b4c9e1b 100644 --- a/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/DocumentView.swift +++ b/ios-in-person-verifier-tutorial-sample-app/In person verification tutorial/DocumentView.swift @@ -116,7 +116,8 @@ class DocumentViewModel { init(from presentation: MobileCredentialPresentation) { self.docType = presentation.docType self.verificationResult = presentation.verificationResult.verified ? "Verified" : "Invalid" - self.verificationFailedReason = presentation.verificationResult.reason?.message + // From v6.0.0, the failure detail is exposed via `failureType` (was `reason`). + self.verificationFailedReason = presentation.verificationResult.failureType?.message self.namespacesAndClaims = presentation.claims?.reduce(into: [String: [String: String]]()) { result, outerElement in let (outerKey, innerDict) = outerElement diff --git a/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/ContentView.swift b/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/ContentView.swift index f4115f1..3c70250 100644 --- a/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/ContentView.swift +++ b/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/ContentView.swift @@ -68,8 +68,11 @@ final class VerifierViewModel: ObservableObject { @Published var navigationPath = NavigationPath() @Published var receivedDocuments: [MobileCredentialPresentation] = [] + // From v6.0.0, the applicationId is supplied via PlatformConfiguration (it is no longer passed + // to requestMobileCredentials) and also drives SDK Tethering. let platformConfiguration = PlatformConfiguration( - tenantHost: Constants.tenantHost + tenantHost: Constants.tenantHost, + applicationId: Constants.applicationID ) var mobileCredentialVerifier: MobileCredentialVerifier @@ -95,11 +98,20 @@ final class VerifierViewModel: ObservableObject { // platform configuration (your tenant host). Trusted issuer certificates are managed // in your MATTR VII tenant rather than in the app for this workflow. init() { - do { - mobileCredentialVerifier = MobileCredentialVerifier.shared - try mobileCredentialVerifier.initialize(platformConfiguration: platformConfiguration) - } catch { - print(error.localizedDescription) + mobileCredentialVerifier = MobileCredentialVerifier.shared + // From v6.0.0, initialize is asynchronous and drives SDK Tethering: on first launch it + // registers this app instance with the tenant and obtains a license, so it can throw + // failedToRegister and invalidLicense. + Task { + do { + try await mobileCredentialVerifier.initialize(platformConfiguration: platformConfiguration) + } catch MobileCredentialVerifierError.failedToRegister { + // Registration with the MATTR VII tenant failed — check connectivity and configuration. + } catch MobileCredentialVerifierError.invalidLicense { + // The SDK license is missing, invalid, or expired. + } catch { + print(error.localizedDescription) + } } } @@ -111,10 +123,12 @@ final class VerifierViewModel: ObservableObject { Task { @MainActor in receivedDocuments = [] do { + // From v6.0.0, applicationId is no longer passed here (it comes from + // PlatformConfiguration) and challenge is optional — omit it to let the SDK generate + // a secure challenge, or pass your own to bind the response to this request. let onlinePresentationResult = try await mobileCredentialVerifier.requestMobileCredentials( request: [mobileCredentialRequest], - challenge: UUID().uuidString, - applicationId: Constants.applicationID + challenge: UUID().uuidString ) guard let receivedCredentials = onlinePresentationResult.mobileCredentialResponse?.credentials else { let errorMessage = onlinePresentationResult.error?.message ?? "No error message" diff --git a/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/DocumentView.swift b/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/DocumentView.swift index 5e25804..005b68b 100644 --- a/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/DocumentView.swift +++ b/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/DocumentView.swift @@ -111,7 +111,8 @@ class DocumentViewModel: ObservableObject { init(from presentation: MobileCredentialPresentation) { self.docType = presentation.docType self.verificationResult = presentation.verificationResult.verified ? "Verified" : "Invalid" - self.verificationFailedReason = presentation.verificationResult.reason?.message + // From v6.0.0, the failure detail is exposed via `failureType` (was `reason`). + self.verificationFailedReason = presentation.verificationResult.failureType?.message self.namespacesAndClaims = presentation.claims?.reduce(into: [String: [String: String]]()) { result, outerElement in let (outerKey, innerDict) = outerElement From 5d14584a937b6ad5831a84e68aaf7f3d847e3e34 Mon Sep 17 00:00:00 2001 From: Avner Matan <137777701+avner-m@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:50:31 +1200 Subject: [PATCH 2/2] fix(verifier): handle OnlinePresentationSessionResult enum in iOS remote sample From iOS Verifier SDK v6.0.0, OnlinePresentationSessionResult is a @frozen enum with success(sessionId:challenge:mobileCredentialResponse:) and failure(sessionId:challenge:error:) cases. Replace the old nullable struct-property access with switch/case, matching the Android sample and the v6.0.0 migration guide. --- .../Remote verification tutorial/ContentView.swift | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/ContentView.swift b/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/ContentView.swift index 3c70250..6249e64 100644 --- a/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/ContentView.swift +++ b/ios-remote-verification-tutorial-sample-app/Remote verification tutorial/ContentView.swift @@ -130,12 +130,17 @@ final class VerifierViewModel: ObservableObject { request: [mobileCredentialRequest], challenge: UUID().uuidString ) - guard let receivedCredentials = onlinePresentationResult.mobileCredentialResponse?.credentials else { - let errorMessage = onlinePresentationResult.error?.message ?? "No error message" - print("No response received: \(errorMessage)") + // From v6.0.0, OnlinePresentationSessionResult is a @frozen enum with success and + // failure cases, so branch over it with switch/case instead of inspecting nullable + // fields. success carries (sessionId, challenge, mobileCredentialResponse); failure + // carries (sessionId, challenge, error). + switch onlinePresentationResult { + case .success(_, _, let mobileCredentialResponse): + receivedDocuments = mobileCredentialResponse?.credentials ?? [] + case .failure(_, _, let error): + print("No response received: \(error.message)") return } - receivedDocuments = receivedCredentials } catch { print(error.localizedDescription) }