The proximity verification workflow described in this tutorial is based on the
ISO/IEC 18013-5:2021 standard. If you are unfamiliar
with this standard, refer to the following Docs for more information:
We assume you have experience developing applications in the relevant programming languages and
frameworks (Swift for iOS, Kotlin for Android, and JavaScript/TypeScript for React Native).
If you need to get a verifier solution up and running quickly with minimal
development resources and in-house domain expertise, talk to
us about our white-label MATTR GO
Verify which might be a good fit for you.
This should result in the the following framework being added to your project:
Step 3: Add Bluetooth permissions
The SDK requires access to the mobile device Bluetooth capabilities as part of the proximity
presentation workflow.
Configure these permissions in the Info
tab of the Application target:
Step 4: Run the application
Select Run and make sure the application launches with a “Hello, world!” text in the middle of
the display, as shown in the following image:
Create a new directory named repo in your project's folder.
Unzip the mobile-credential-verifier-*version*.zip file and copy the unzipped
global folder into the new repo folder.
Open the settings.gradle.kts file in the VerifierTutorial folder and add the following Maven
repository to the dependencyResolutionManagement.repositories block:
settings.gradle.kts
maven { url = uri("repo")}
Open the app/build.gradle.kts file in your app folder and add the following dependencies to the
dependencies block:
Open the tutorial project in your code editor. You can find it in the
sample-apps/react-native-mdocs-verifier-tutorial/react-native-mdocs-verifier-tutorial-starter/
directory.
You can find the completed tutorial code in the sample-apps/react-native-mdocs-verifier-tutorial/react-native-mdocs-verifier-tutorial-complete
directory and use it as a reference as you work along this tutorial.
Step 2: iOS Application configuration
Open the app.config.ts file and update the bundleIdentifier value under the
// Update the bundle identifier comment to a unique value for your application, e.g.
com.mycompany.myapp.
app.config.ts
bundleIdentifier: "com.mycompany.myapp",
iOS requires each app to have a unique bundle identifier for App Store and development
environments.
Add the following camera and Bluetooth permissions to the ios.infoPlist object under the
// Add necessary permissions for camera and Bluetooth comment:
app.config.ts
NSCameraUsageDescription: "Camera is used to scan QR codes.",NSBluetoothAlwaysUsageDescription: "This app uses Bluetooth to communicate with verifiers or holders.",NSBluetoothPeripheralUsageDescription: "This app uses Bluetooth to communicate with verifiers or holders.",
These permissions are required for the app to use the camera for QR
code scanning, and Bluetooth for proximity communication with the holder's app.
Step 3: Configure the app plugins
Add the following code under the // Configure the app plugins comment to import required plugin
configurations:
The SDK requires platform-specific configurations to work correctly. A plugin file specifically for Android has already
been created in your project root directory. You can also follow the instructions in the
mDocs Verifier
SDK Docs to perform this platform-specific configuration manually.
Step 4: Install the dependencies
Open a terminal in the project's root and navigate to the starter project directory:
Navigate to the project directory
cd sample-apps/react-native-mdocs-verifier-tutorial/react-native-mdocs-verifier-tutorial-starter/
Install the application dependencies:
Install dependencies
yarn install
Step 5: Generate the iOS and Android project files
Run the following command to generate the iOS and Android project files:
Generate project files
yarn expo prebuild
You should now see the ios and android folders in your project root.
Step 6: Start the application
Connect your testing device(s) and run the following command to start the application(s):
iOS
Run iOS application
yarn ios --device
Android
Run Android application
yarn android --device
Nice work, your application is now all set to begin using the SDK!
The first capability you will build into your app is to initialize the SDK so that your app can use
SDK functions and classes. To achieve this, we need to import the MobileCredentialVerifierSDK
framework and then initialize the MobileCredentialVerifier class.
Open the ContentView file in your new project and replace any existing code with the
following:
ContentView
import SwiftUIimport Combine// Initialize SDK - Step 2.1: Import MobileCredentialVerifierSDKstruct ContentView: View { @ObservedObject var viewModel: VerifierViewModel = VerifierViewModel() var body: some View { NavigationStack(path: $viewModel.navigationPath) { VStack { Button("Scan QR Code") { viewModel.navigationPath.append(NavigationState.scanQRCode) } .padding() Button("View Response") { viewModel.navigationPath.append(NavigationState.viewResponse) } .padding() } .navigationDestination(for: NavigationState.self) { destination in switch destination { case .scanQRCode: codeScannerView case .viewResponse: presentationResponseView } } } .task { await viewModel.setupCertificates() } } // MARK: Verification Views var codeScannerView: some View { // Verify mDocs - Step 2.4: Create QRScannerView EmptyView() } var presentationResponseView: some View { // Verify mDocs - Step 4.2: Create PresentationResponseView EmptyView() }}// MARK: VerifierViewModelfinal class VerifierViewModel: ObservableObject { @Published var navigationPath = NavigationPath() // Initialize SDK - Step 2.2: Add MobileCredentialVerifier var // Verify mDocs - Step 1.1: Create MobileCredentialRequest instance // Verify mDocs - Step 1.2: Create receivedDocuments variable // Initialize SDK - Step 2.3: Initialize the SDK // Setup certificates - Step 2: Add trusted issuer certificates func setupCertificates() async { print("This method will add the trust anchor to the sdk storage") }}// MARK: Proximity Presentationextension VerifierViewModel { func setupProximityPresentationSession(_ deviceEngagementString: String) { // Verify mDocs - Step 3.2: Create setupProximityPresentationSession print("This method will use qr code string do setup proximity session") } func sendDeviceRequest() { // Verify mDocs - Step 3.3: Create sendDeviceRequest function print("This method will send preconfigured device request to holder app") }}// Verify mDocs - Step 3.1: Extend VerifierViewModel class// MARK: - Navigationenum NavigationState: Hashable { case scanQRCode case viewResponse}
This will serve as the basic structure for your application. We will copy and paste
different code snippets into specific locations to achieve the different functionalities.
These locations are indicated by comments that reference both the section and the step.
We recommend copying and pasting the comment text in Xcode search field (e.g.
// Initialize SDK - Step 2.2: Add MobileCredentialVerifier var) to easily locate it in the code.
Open the app/src/main/java/com.example.verifiertutorial/MainActivity.kt file in your project and
replace any existing code with the following:
This will serve as the basic structure for your application. We will copy and
paste different code snippets into specific locations in this codebase to
achieve the different functionalities. These locations are indicated by
comments that reference both the section and the step.
Open the App.tsx file in your project and replace the existing code with the skeleton structure:
App.tsx
import { type MobileCredentialResponse, addTrustedIssuerCertificates, createProximityPresentationSession, getTrustedIssuerCertificates, initialize, sendProximityPresentationRequest, terminateProximityPresentationSession,} from "@mattrglobal/mobile-credential-verifier-react-native";// import { QRScannerModal } from "./QRScannerModal";// import { VerificationResultsModal } from "./VerificationResultsModal";import { useCameraPermissions } from "expo-camera";import { StatusBar } from "expo-status-bar";import { useEffect, useState } from "react";import { ActivityIndicator, Alert, SafeAreaView, Text, TouchableOpacity, View } from "react-native";import { styles } from "./styles";export default function App() {// State variables for SDK initialization, UI and loading messagesconst [isSDKInitialized, setIsSDKInitialized] = useState(false);const [loadingMessage, setLoadingMessage] = useState<string | false>(false);const [verificationResults, setVerificationResults] = useState<MobileCredentialResponse | null>(null);// Modal statesconst [isScanning, setIsScanning] = useState(false);const [showVerificationResults, setShowVerificationResults] = useState(false);const [permission, requestPermission] = useCameraPermissions();// Initialize SDK - Step 2.1: Initialize the SDK// Verify mDocs - Step 1.2: Create handleQRCodeDetected functionreturn ( <SafeAreaView style={styles.container}> <StatusBar style="auto" /> <View style={styles.header}> <Text style={styles.title}>mDocs Verifier</Text> </View> {loadingMessage ? ( <View style={[styles.content, styles.center]}> <ActivityIndicator size="large" color="#007AFF" /> <Text style={styles.loadingText}>{loadingMessage}</Text> </View> ) : ( <View style={styles.content}> <View style={styles.buttonContainer}> {/* Verify mDocs - Step 1.5: Create Scan QR Code Button */} </View> {!isSDKInitialized && <Text style={styles.errorText}>SDK not initialized. Please restart the app.</Text>} </View> )} {/* Verify mDocs - Step 1.4: Use QRScannerModal */} {/* Verify mDocs - Step 2.3: Use VerificationResultModal */} </SafeAreaView>);}
This will serve as the basic structure for your application. We will add code to specific locations to achieve the different functionalities. These locations are indicated by comments that reference both the section and the step.
We recommend using your editor's search functionality to locate comments like // Initialize SDK - Step 1.3: Initialize the SDK when adding new code.
Add the following code after the
// Initialize SDK - Step 2.1: Import MobileCredentialVerifierSDK comment to import
MobileCredentialVerifierSDK and gain access to the SDK's capabilities:
ContentView
import MobileCredentialVerifierSDK
Add the following code after the
// Initialize SDK - Step 2.2: Add MobileCredentialVerifier var comment to create a variable
that holds the mobileCredentialVerifier instance:
ContentView
var mobileCredentialVerifier: MobileCredentialVerifier
Add the following code after the // Initialize SDK - Step 2.3: Initialize the SDK comment to
assign a shared instance of the class to our mobileCredentialVerifier variable and initialize
the SDK:
Once the app launches you will see a screen with three buttons, each leading to an empty view. In
the following steps, you will implement proximity presentation functionalities into these views.
Add the following code after the // Initialize SDK - Step 1.1: Initialize the SDK comment to
initialize the SDK:
We recommend leaving the comment text (e.g. // Initialize SDK - Step 1.1: Initialize the SDK) even after you have pasted the code snippet, as it
will later help you to easily locate the step in the code.
Add the following code after the // Initialize SDK - Step 2.1: Initialize the SDK comment to initialize the SDK:
We recommend leaving the comment text (e.g. // Initialize SDK - Step 2.1: Initialize the SDK) even after you have pasted the code snippet, as it will later help you to easily locate the step in the code.
App.tsx
useEffect(() => { const initializeSDK = async () => { try { setLoadingMessage("Initializing SDK..."); const result = await initialize(); if (result.isErr()) { console.error("Failed to initialize SDK:", result.error); Alert.alert("Error", "Failed to initialize the verifier SDK"); return; } setIsSDKInitialized(true); // Setup certificates - Step 3: Register the trusted IACA certificate on first launch } catch (error) { console.error("Failed to initialize SDK:", error); Alert.alert("Error", "Failed to initialize the verifier SDK"); } finally { setLoadingMessage(false); } }; initializeSDK();}, []);
Once the SDK is initialized, the next step is to add a trusted issuer certificate.
Every mDoc is signed using a certificate chain, also known as a chain of trust. To verify a presented mDoc, your application must confirm that this chain leads back to a trusted root certificate, called an IACA.
To do this, your application must provide the SDK with the IACA certificates for every issuer it should trust. In this tutorial, you will add the IACA certificate for the MATTR Labs test issuer, which was used to issue the credential you will verify.
Create a new file called IACAs.swift and add the following code:
This file contains the root certificate of the MATTR Labs test issuer.
Return to ContentView.swift file and replace the setupCertificates function body after the
comment // Setup certificates - Step 2: Add trusted issuer certificates with the following:
This file contains the root certificate of the MATTR Labs test issuer.
Return to the MainActivity.kt file and add the following code after the // Setup certificates - Step 2.2: Add trusted issuer certificates comment to add the MATTR Labs test issuer certificate to the SDK:
This file contains the root certificate of the MATTR Labs test issuer.
Back in App.tsx, import the certificate alongside your existing imports:
App.tsx
import { MONTCLIFF_DMV_IACA } from "./certificates";
Add the following code after the // Setup certificates - Step 3: Register the trusted IACA certificate on first launch comment to register the MATTR Labs test issuer certificate with the SDK the first time the app runs:
In this part we will build the components that enable a verifier app to verify an mDoc presented via
a proximity workflow as per ISO/IEC 18013-5:2021:
To achieve this, your application must be able to:
Create a presentation request that defines the information required for verification.
Scan and process a QR code presented by a wallet application. Your application must retrieve the information from that QR code and use it to establish a secure connection between the verifier and holder devices.
Your verifier application then uses this secure connection to send a presentation request to which the holder wallet application responds with a presentation response.
Finally, the SDK verifies any mDocs included in the response, stores the verification results in a variable and makes them available to your application to display.
Your application will use the SDK's createProximityPresentationSession function
that takes a string retrieved from the QR code and uses it to establish a proximity presentation
session with the wallet application and initiate the presentation workflow.
This function takes a listener argument of type ProximityPresentationSessionListener delegate,
which will receive proximity presentation session events.
Step 1: Create a presentation request
As a verifier, you can select what information you request for verification. Your application
implements this by creating a
MobileCredentialRequest
instance to define the required information, and a new variable to hold the response from the wallet
application.
Open the ContentView file and add the following code under the
// Verify mDocs - Step 1.1: Create MobileCredentialRequest instance comment to define what
information to request from the wallet application user:
The requested credential type (e.g. org.iso.18013.5.1.mDL).
The claims required for verification (e.g. family_name).
The requested namespace (e.g. org.iso.18013.5.1).
Whether or not the verifier intends to persist the claim value (true/false).
For the verification to be successful, the presented credential must include the referenced
claim against the specific namespace defined in the request. Our example requests the
birth_date under the org.iso.18013.5.1 namespace. If a wallet responds to this request with
a credential that includes a birth_date but rather under the org.iso.18013.5.1.US namespace,
the claim will not be verified.
To simplify the tutorial, this is a hardcoded request. However, once you are
comfortable with the basic functionalities you can create a UI in your
verifier application that enables the user to create different requests on the
fly by selecting different claims to include. Check out our GO Verify
app to see this in action.
Add the following code under the Verify mDocs - Step 1.2: Create receivedDocuments variable
comment to create a new receivedDocuments variable that will hold the response from the wallet
application:
ContentView
@Published var receivedDocuments: [MobileCredentialPresentation] = []
Your application now has an existing credential request to share, and a variable to hold any
incoming responses. In the next step we will build the capabilities to send this request and handle
the response.
Step 2: Scan and process a QR code
As defined in ISO/IEC 18130-5:2021, a
proximity presentation workflow is always initiated by the
holder (wallet application user), who must create a QR code for the verifier to scan in order to
initiate the device engagement phase.
This means that your verifier application must be able to scan and process this QR code. For ease of
implementation, we will use a third party framework to achieve this.
Create a new swift file named
QRScannerView and add the following code into it to implement the QR scanning capability:
QRScannerView
import SwiftUIimport CodeScannerimport AVFoundationstruct QRScannerView: View { private let completionHandler: (String) -> Void init(completion: @escaping (String) -> Void) { completionHandler = completion } var body: some View { CodeScannerView(codeTypes: [.qr]) { result in switch result { case .failure(let error): print(error.localizedDescription) case .success(let result): print(result.string) completionHandler(result.string) } } }}
Back in the ContentView file, replace the EmptyView() under the
// Verify mDocs - Step 2.4: Create QRScannerView comment with the following code to create a
new app view that the user will use to scan a QR code:
ContentView
QRScannerView( completion: { string in viewModel.setupProximityPresentationSession(string) } )
Run
the app and select the Scan QR Code button. You should be navigated to the new
QRScannerView where you can use the camera to scan a QR code.
Next we will build the logic that handles this QR code to establish a secure connection with the
wallet application.
Step 3: Exchange presentation request and response
Add the following code under the Verify mDocs - Step 3.1: Extend VerifierViewModel class to
extend the VerifierViewModel class with the
ProximityPresentationSessionListener
protocol:
ContentView
extension VerifierViewModel: ProximityPresentationSessionListener { public func onEstablished() { sendDeviceRequest() } public func onTerminated(error: (any Error)?) { print("Session Terminated") } }
Now, as soon as a connection is established, the app will send a device request. You will
implement the functionality of sendDeviceRequest() in VerifierViewModel later in the
tutorial.
Replace the print statement under the
// Verify mDocs - Step 3.2: Create setupProximityPresentationSession comment with the
following code to call the SDK's
createProximityPresentationSession
function, passing a device engagement string (retrieved from a QR code) and self as a listener
to create a proximity presentation session:
Replace the print statement under the
// Verify mDocs - Step 3.3: Create sendDeviceRequest function comment with following code to
implement the logic to send a device request:
ContentView
Task { @MainActor in receivedDocuments = [] do { // Navigate to response screen navigationPath.append(NavigationState.viewResponse) // Request mDocs let deviceResponse = try await mobileCredentialVerifier.sendProximityPresentationRequest( request: [mobileCredentialRequest] ) // Assign new values from the response receivedDocuments = deviceResponse.credentials ?? [] // Terminate session after response is received (optional) await mobileCredentialVerifier.terminateProximityPresentationSession() } catch { print(error) receivedDocuments = [] } }
Store the wallet response in the deviceResponse variable. This includes the verification
results of any credentials included in the response.
Store the verification results in the receivedDocuments variable.
Terminate the presentation session once the response is received.
Step 4: Display verification results
Create a new file named DocumentView and add the following code to display available
verification results:
DocumentView
import MobileCredentialVerifierSDKimport SwiftUIimport Combine struct DocumentView: View { var viewModel: DocumentViewModel var body: some View { VStack(alignment: .leading, spacing: 10) { Text(viewModel.docType) .font(.title) .fontWeight(.bold) .padding(.bottom, 5) Text(viewModel.verificationResult) .font(.title) .fontWeight(.bold) .foregroundStyle(viewModel.verificationFailedReason == nil ? .green : .red) .padding(.bottom, 5) if let verificationFailedReason = viewModel.verificationFailedReason { Text(verificationFailedReason) .font(.title3) .fontWeight(.bold) .foregroundStyle(.red) .padding(.bottom, 5) } ForEach(viewModel.namespacesAndClaims.keys.sorted(), id: \.self) { key in VStack(alignment: .leading, spacing: 5) { Text(key) .font(.headline) .padding(.vertical, 5) .padding(.horizontal, 10) .background(Color.gray.opacity(0.2)) .cornerRadius(5) ForEach(viewModel.namespacesAndClaims[key]!.keys.sorted(), id: \.self) { claim in HStack { Text(claim) .fontWeight(.semibold) Spacer() Text(viewModel.namespacesAndClaims[key]![claim]! ?? "") .fontWeight(.regular) } .padding(.vertical, 5) .padding(.horizontal, 10) .background(Color.white) .cornerRadius(5) .shadow(radius: 1) } } .padding(.vertical, 5) } if !viewModel.claimErrors.isEmpty { Text("Failed Claims:") .font(.headline) .padding(.vertical, 5) ForEach(viewModel.claimErrors.keys.sorted(), id: \.self) { key in VStack(alignment: .leading, spacing: 5) { Text(key) .font(.headline) .padding(.vertical, 5) .padding(.horizontal, 10) .background(Color.gray.opacity(0.2)) .cornerRadius(5) ForEach(viewModel.claimErrors[key]!.keys.sorted(), id: \.self) { claim in HStack { Text(claim) .fontWeight(.semibold) Spacer() Text(viewModel.claimErrors[key]![claim]! ?? "") .fontWeight(.regular) } .padding(.vertical, 5) .padding(.horizontal, 10) .background(Color.white) .cornerRadius(5) .shadow(radius: 1) } } .padding(.vertical, 5) } } } .padding() .background(RoundedRectangle(cornerRadius: 10).fill(Color.white).shadow(radius: 5)) .padding(.horizontal) } } // MARK: DocumentViewModel class DocumentViewModel: ObservableObject { let docType: String let namespacesAndClaims: [String: [String: String?]] let claimErrors: [String: [String: String?]] let verificationResult: String let verificationFailedReason: String? init(from presentation: MobileCredentialPresentation) { self.docType = presentation.docType self.verificationResult = presentation.verificationResult.verified ? "Verified" : "Invalid" self.verificationFailedReason = presentation.verificationResult.reason?.message self.namespacesAndClaims = presentation.claims?.reduce(into: [String: [String: String]]()) { result, outerElement in let (outerKey, innerDict) = outerElement result[outerKey] = innerDict.mapValues { $0.textRepresentation } } ?? [:] self.claimErrors = presentation.claimErrors?.reduce(into: [String: [String: String]]()) { result, outerElement in let (outerKey, innerDict) = outerElement result[outerKey] = innerDict.mapValues { "\($0)" } } ?? [:] } } // MARK: Helper extension MobileCredentialElementValue { var textRepresentation: String { switch self { case .bool(let bool): return "\(bool)" case .string(let string): return string case .int(let int): return "\(int)" case .unsigned(let uInt): return "\(uInt)" case .float(let float): return "\(float)" case .double(let double): return "\(double)" case let .date(date): let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .none return dateFormatter.string(from: date) case let .dateTime(date): let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .short return dateFormatter.string(from: date) case .data(let data): return "Data \(data.count) bytes" case .map(let dictionary): let result = dictionary.mapValues { value in value.textRepresentation } return "\(result)" case .array(let array): return array.reduce("") { partialResult, element in partialResult + element.textRepresentation } .appending("") @unknown default: return "Unknown type" } } }
The DocumentView file comprises the following elements:
DocumentView : Basic UI layout for viewing received documents and verification results.
DocumentViewModel : This class takes MobileCredentialPresentation and converts its
elements into strings to display in the DocumentView.
Extension of MobileCredentialElementValue which converts the values of received claims into
a human-readable format.
Return to the ContentView file and replace the EmptyView() under the
// Verify mDocs - Step 4.2: Create PresentationResponseView comment with the following code to
display the DocumentView view when verification results are available:
Step 1: Create a screen for scanning the credential offer
As defined in ISO/IEC 18130-5:2021, a
proximity presentation workflow is always initiated by the
holder (wallet application user), who must create a QR code for the verifier to scan in order to
initiate the device engagement phase.
This means that your verifier application must be able to scan and process this QR code. For ease of
implementation, we will use a third party framework to achieve this.
In the ScanQrScreen.kt file, add the following code under the
// Verify mDocs - Step 1.4: Add QR scan callback comment to define a callback that is called
when the QR code was successfully scanned (we will implement the callback logic at a later
stage):
Add the following code under the // Verify mDocs - Step 1.5: Add screen content comment to
define the main UI of the screen:
ScanQrScreen.kt
@Composable private fun Content(activity: Activity, navController: NavController) { val context = LocalContext.current val barcodeView = remember { DecoratedBarcodeView(context) } val coroutineScope = rememberCoroutineScope() var isQrScanned by remember { mutableStateOf(false) } val barcodeCallback = remember { BarcodeCallback { result -> onQrScanned(activity, result.text, coroutineScope, navController) barcodeView.pause() isQrScanned = true } } DisposableEffect(Unit) { barcodeView.decodeContinuous(barcodeCallback) barcodeView.resume() onDispose { barcodeView.pause() } } if (!isQrScanned) { AndroidView(factory = { barcodeView }, modifier = Modifier.fillMaxSize()) } else { Box(Modifier.fillMaxSize()) { CircularProgressIndicator(Modifier.align(Alignment.Center)) } } }
Please have a quick look at the code. The screen will show a QR scanning view. As soon as the QR
code is captured, it shows a progress spinning wheel, and calls onQrScanned function.
Add the following code under the // Verify mDocs - Step 1.6: Add permission request logic
comment to define a basic logic for requesting the camera access permission at runtime:
ScanQrScreen.kt
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)val requestPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {}LaunchedEffect(cameraPermissionState) { if (!cameraPermissionState.status.isGranted) { requestPermissionLauncher.launch(Manifest.permission.CAMERA) }}if (cameraPermissionState.status.isGranted) Content(activity, navController)
Back in the MainActivity file, add the following code under the
// Verify mDocs - Step 1.7: Add "Scan QR" screen call to connect the created screen to the
navigation graph:
MainActivity.kt
ScanQrScreen(this@MainActivity, navController)
Run the app and select the Scan QR
Code button. You should be navigated to the new QRScannerView where you can use the camera to
scan a QR code.
Now we will build the logic that handles this QR code to establish a secure connection with the
wallet application.
Step 2: Create a presentation request
As a verifier, you can select what information you request for verification. Our application
implements this by creating a
MobileCredentialRequest
instance to define the required information, and a new variable to hold the response from the wallet
application.
In the ScanQrScreen.kt file, add the following code under the
// Verify mDocs - Step 2.1: Create a sample request comment to define what information to
request from the holder's application:
The requested credential type (e.g. org.iso.18013.5.1.mDL).
The claims required for verification (e.g. given_name).
The requested namespace (e.g. org.iso.18013.5.1).
Whether or not the verifier intends to persist the claim value (true/false).
For the verification to be successful, the presented credential must include the referenced
claim against the specific namespace defined in the request. Our example requests the
birth_date under the org.iso.18013.5.1 namespace. If a wallet responds to this request with
a credential that includes a birth_date but rather under the org.iso.18013.5.1.US namespace,
the claim will not be verified.
To simplify the tutorial, this is a hardcoded request. However, once you are
comfortable with the basic functionalities you can create a UI in your
verifier application that enables the user to create different requests on the
fly by selecting different claims to include. See our GO Verify
app as an example.
Back in the MainActivity.kt file, add the following code under the
// Verify mDocs - Step 2.2: Add shared data comment to create a new credentialResponse
variable that will hold the response from the holder's application:
MainActivity.kt
object SharedData { var credentialResponse: MobileCredentialResponse? = null }
Now your application has an existing request to share, and a variable to hold any incoming
responses. We can now proceed to build the capabilities to send the request and handle the response.
Step 3: Exchange presentation request and response
In ScanQrScreen.kt, add the following code under the
// Verify mDocs - Step 3.1: Create session listener to define a listener that will react to the
proximity presentation session lifecycle events:
ScanQrScreen.kt
private class SessionListener( private val coroutineScope: CoroutineScope, private val continuation: Continuation<MobileCredentialResponse>) : ProximityPresentationSessionListener { override fun onEstablished() { coroutineScope.launch { // Verify mDocs - Step 3.3: Request credentials } } override fun onTerminated(error: Throwable?) { /* no-op */ } override fun onError(error: Throwable?) { error?.let { continuation.resumeWithException(it) } }}
Add the following code under the // Verify mDocs - Step 3.2: Create session to create proximity
presentation session and register a session listener:
We pass Continuation to the listener. It will be resumed either with
MobileCredentialResponse if the whole presentation flow is successful, or with an exception if
there was an issue during any stage of the credentials presentation.
Add the following code under the // Verify mDocs - Step 3.3: Request credentials to request the
mobile credentials:
Calls
sendProximityPresentationRequest
function as soon as the session is established. The function accepts a list of
MobileCredentialRequest, sends the requests to the wallet application, receives a response from the wallet application,
and verifies any mDocs included in the response.
Stores the response in the SharedData.credentialResponse value.
Handles the exceptions, if they were thrown from the above calls. An exception can be thrown if,
for example, the Bluetooth connection between the Holder and Verifier devices was interrupted
during the session.
Now that we have the verification results stored, you can implement different business logics to
handle the results.
For this tutorial, we will display these results to the verifier app user, individually indicating
the verification status of each claim included in the request.
Step 4: Display verification results
Add the following code under the // Verify mDocs - Step 4.1: Handle response to navigate the
user to the response screen, where they can see the retrieved credentials, if the retrieval was
successful:
Back in the MainActivity.kt file, add the following code under the
// Verify mDocs - Step 4.4: Add "View Response" screen call comment to connect the created
composable to the navigation graph:
MainActivity.kt
ViewResponseScreen()
Return to the ViewResponseScreen.kt screen and add the following code under the
// Verify mDocs - Step 4.5: Define content comment to define the basic UI for displaying the
response details to the verifier application user:
ViewResponseScreen.kt
val credential = SharedData.credentialResponse?.credentials?.firstOrNull()if (credential == null || SharedData.credentialResponse?.credentialErrors != null) { // Verify mDocs - Step 4.6: Show error} else { Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(4.dp), ) { // Verify mDocs - Step 4.10: Show credential verification status // Verify mDocs - Step 4.9: Show retrieved claims and errors }}
While our SDK
allows
to request multiple document types (and thus, multiple credentials) at the
same time, for the tutorial simplicity we requested only one document type,
and expect to see only one mobile credential as the response. Because of that,
we take and handle only the first element from the retrieved credentials list.
Add the following code under the // Verify mDocs - Step 4.6: Show error comment to show an
error message in case the response is empty or if there were major errors during the response
retrieval:
ViewResponseScreen.kt
Box(Modifier.fillMaxSize()) { Text("There were errors while receiving the response", Modifier.align(Alignment.Center))}
Add the following code under the // Verify mDocs - Step 4.7: Map a claim or an error to string
comment to map the received claim value (or a claim error) to a string:
ViewResponseScreen.kt
private fun Any.claimToUiString() = when (this) { is MobileCredentialElement -> { when (this) { is MobileCredentialElement.ArrayElement, is MobileCredentialElement.DataElement, is MobileCredentialElement.MapElement -> this::class.simpleName ?: "Unknown element" else -> value.toString() } } else -> "Not returned"}
Claim error here means that the presentation session has completed
successfully, without interruption, and the mobile credentials were received
and verified, but some of the claim values were not sent to the verifier.
Usually this can happen if they were absent in the document on the Holder
side. For example, Verifier requested family name, given name, date of
birth, and photo, but the document on the Holder contained only family
name, given name_, and date of birth. Another case is if the wallet user
did not give the consent for sharing some of the claims.
Add the following code under the // Verify mDocs - Step 4.8: Display claims comment to create a
function that displays the retrieved and failed claims to the verifier application user:
Add the following code under the // Verify mDocs - Step 4.9: Show retrieved claims and errors
comment to create the UI for showing the retrieved and failed claims on the screen:
Add the following code under the
// Verify mDocs - Step 4.10: Show credential verification status comment to show the overall
verification status:
ViewResponseScreen.kt
val statusStyle = MaterialTheme.typography.titleLargeif (credential.verificationResult.verified) { Text("Verified", style = statusStyle, color = Color.Green)} else { Text("Not verified", style = statusStyle, color = Color.Red)}
Step 1: Create a component for scanning a QR code presented by the holder
Create a new file called QRScannerModal.tsx and add the following code into it to implement the QR
scanning capability:
QRScannerModal.tsx
import { CameraView } from "expo-camera";import { useEffect, useRef, useState } from "react";import { Alert, Modal, SafeAreaView, Text, TouchableOpacity, View } from "react-native";import { styles } from "./styles";interface QRScannerModalProps { visible: boolean; onClose: () => void; permission: any; requestPermission: () => Promise<any>; onQRCodeDetected: (data: string) => void;}export function QRScannerModal({ visible, onClose, permission, requestPermission, onQRCodeDetected,}: QRScannerModalProps) { const [scanned, setScanned] = useState(false); const [scanningEnabled, setScanningEnabled] = useState(true); const handlerCalledRef = useRef(false); const handleBarCodeScanned = ({ data }: { data: string }) => { if (!scanningEnabled || scanned || handlerCalledRef.current) { return; } // Immediately mark as handled and disable scanning handlerCalledRef.current = true; setScanningEnabled(false); setScanned(true); console.log(`Scanned barcode with data: ${data}`); // Check if data starts with "mdoc:" if (!data || !data.startsWith("mdoc:")) { Alert.alert( "Invalid QR Code", "The QR code must be an mDoc QR code starting with 'mdoc:'. Please scan a valid mDoc QR code.", [ { text: "Try Again", onPress: () => resetScanner(), }, ] ); return; } console.log("Valid mDoc QR code detected:", data); // Close modal immediately to stop camera onClose(); // Call handler after modal is closed to prevent camera from firing again setTimeout(() => { onQRCodeDetected(data); }, 300); }; const resetScanner = () => { handlerCalledRef.current = false; setScanned(false); setScanningEnabled(true); }; const handleClose = () => { resetScanner(); onClose(); }; // Reset scanner state when modal becomes visible useEffect(() => { if (visible) { resetScanner(); } }, [visible]); if (!visible) return null; return ( <Modal visible={visible} animationType="slide" transparent={false}> <SafeAreaView style={styles.container}> <View style={styles.header}> <Text style={styles.title}>QR Code Scanner</Text> <TouchableOpacity onPress={handleClose}> <Text style={styles.buttonTextBold}>Close</Text> </TouchableOpacity> </View> <View style={styles.content}> {!permission ? ( <View style={styles.centeredContent}> <Text style={styles.errorText}>Camera permissions are still loading</Text> </View> ) : !permission.granted ? ( <View style={styles.centeredContent}> <Text style={styles.errorText}>Camera permission is required to scan QR codes</Text> <TouchableOpacity style={[styles.button, styles.marginTop]} onPress={requestPermission}> <Text style={styles.buttonText}>Request Permission</Text> </TouchableOpacity> </View> ) : ( <> {!scanned && ( <CameraView style={styles.camera} facing="back" barcodeScannerSettings={{ barcodeTypes: ["qr"], }} onBarcodeScanned={handleBarCodeScanned} /> )} <View style={styles.qrOverlay}> <View style={styles.qrFrame} /> <Text style={styles.qrOverlayText}> {scanned ? "Processing QR code..." : "Point your camera at a QR code"} </Text> </View> {scanned && ( <View style={styles.scannerControls}> <TouchableOpacity style={[styles.button, styles.buttonSuccess]} onPress={resetScanner}> <Text style={styles.buttonTextBold}>Scan Again</Text> </TouchableOpacity> </View> )} </> )} </View> </SafeAreaView> </Modal> );}
This component uses the expo-camera package and handles camera permissions through props passed from App.tsx.
It's configured to scan QR codes and validates that the scanned data starts with "mdoc:" prefix.
The handleBarCodeScanned function processes the scanned data and calls the onQRCodeDetected callback with the QR code data.
Return to your App.tsx file and add the following code under the // Verify mDocs - Step 1.2: Create handleQRCodeDetected function comment to add a presentation workflow handler. This handler uses the SDK's createProximityPresentationSession and sendProximityPresentationRequest and methods to establish a session and request a credential:
Create the Verification Results modal in a new file called VerificationResultsModal.tsx and pasted the following code into it. MobileCredentialResponse is the type that holds the verification results from the MATTR Verifier SDK. The modal will display the verification results, including any claims and errors.
Return to your App.tsx file and uncomment the VerificationResultsModal import to integrate the verification results modal:
App.tsx
import { VerificationResultsModal } from "./VerificationResultsModal";
Add the following code under the // Verify mDocs - Step 2.3: Use VerificationResultModal comment to display the verification results modal when verification is complete:
You can implement
NFC based device engagement
capabilities (currently supported by the Android Verifier SDK and the React Native SDK for Android platforms only).