light-mode-image
Learn

Learn how to build an application that can verify an mDoc presented via a proximity workflow

Overview

In this tutorial you will use the mDocs mobile verifier SDKs to build an application that can verify an mDoc presented via a proximity workflow as per ISO 18013-5:

Tutorial Workflow

  1. The credential holder presents a QR code generated by their wallet application.
  2. The verifier uses their application to scan the QR code, connect with the wallet and request an mDoc for verification.
  3. The wallet application displays matching credentials to the holder and asks for consent to share them with the verifier.
  4. The verifier application receives the wallet's response and verifies the provided credential.
  5. Verification results are displayed to the verifier.

The result will look something like this:

To achieve this, you will build the following capabilities into your verifier application:

  • Initialize the SDK, so that your application can use its functions and classes.
  • Manage certificates, which enable your application to verify mDocs that were issued by trusted issuers.
  • Scan a QR code presented by a wallet application and establish a secure communication channel.
  • Send presentation requests to the wallet application, receive a presentation response and verify its content.
  • Display the results to the verifier app user.

Tutorial Steps

Prerequisites

Before we get started, let's make sure you have everything you need.

Prior knowledge

  • 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.

Assets

  • As part of your onboarding process you should have been provided with access to the following assets:
    • ZIP file which includes the required framework: (MobileCredentialVerifierSDK-*version*.xcframework.zip).
    • Sample Verifier app: You can use this app for reference as we work through this tutorial.

This tutorial is only meant to be used with the most recent version of the iOS mDocs Verifier SDK.

  • As part of your onboarding process you should have been provided with access to the following assets:
    • ZIP files that include the required libraries:
      • common-*version*.zip
      • verifier-*version*.zip
    • Sample Verifier app: You can use this app for reference as we work through this tutorial.

This tutorial is only meant to be used with the most recent version of the Android mDocs Verifier SDK.

  • As part of your onboarding process you should have been provided with access to the following assets:

This tutorial is only meant to be used with the most recent version of the React Native mDocs Verifier SDK.

Development environment

  • Xcode setup with either:
    • Local build settings if you are developing locally.
    • iOS developer account if you intend to publish your app.

This tutorial uses Expo Go, leveraging Development Builds.

Testing devices

As this tutorial implements a proximity presentation workflow, you will need two different mobile devices to test the end-to-end result:

  • Verifier device:
    • Supported iOS device to run the built Verifier application on, setup with:
      • Bluetooth access.
      • Available internet connection.
  • Holder device:
    • Mobile device with the MATTR GO Hold example app installed and setup with:
      • Biometric authentication.
      • Bluetooth access.
      • Available internet connection.
  • Verifier device:
    • Supported Android device to run the built Verifier application on, setup with:
      • Bluetooth access.
      • Available internet connection.
      • USB debugging enabled.
  • Holder device:
    • Mobile device with the MATTR GO Hold example app installed and setup with:
      • Biometric authentication.
      • Bluetooth access.
      • Available internet connection.
  • Verifier device:
    • Supported iOS or Android device to run the built Verifier application on, setup with:
      • Bluetooth access.
      • Available internet connection.
  • Holder device:
    • Mobile device with the MATTR GO Hold example app installed and setup with:
      • Biometric authentication.
      • Bluetooth access.
      • Available internet connection.

mDoc

  1. Download and install the MATTR GO Hold example app on your holder testing device.

  2. Use the GO Hold example app to claim an mDoc by scanning the following QR code:

    QR Code

Got everything? Let's get going!

Environment setup

Tutorial Step 1

Perform the following steps to setup and configure your development environment:

Step 1: Create a new project

Please follow the detailed instructions to Create a new Xcode Project and add your organization's identifier.

Create a new project

Step 2: Unzip the dependencies file

  1. Unzip the MobileCredentialVerifierSDK-*version*.xcframework.zip file.
  2. Drag the MobileCredentialVerifierSDK-*version*.xcframework folder into your project.
  3. Configure MobileCredentialVerifierSDK.xcframework to Embed & sign.

See Add existing files and folders for detailed instructions.

This should result in the the following framework being added to your project:

Framework added

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:

Privacy capabilities

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:

Application ready

Step 1: Create a new project

  1. Create a new Android Studio project, using the Empty Activity template.

Create a new Android project

  1. Name the project Verifier Tutorial.
  2. Select API 24 as the Minimum SDK version.
  3. Select Kotlin DSL as the Build configuration language.

Project configuration

  1. Select Finish.
  2. Sync the project with Gradle files.

Step 2: Add required dependencies

  1. Select the Project view.

    Project view

  2. Create a new directory named repo in your project's folder.

  3. Unzip the common-*version*.zip and verifier-*version*.zip files and copy the unzipped global folders into the new repo folder, merging the top-level directories.

    Unzipped files copied

  4. 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")
    }
  5. Open the app/build.gradle.kts file in your app folder and add the following dependencies to the dependencies block:

app/build.gradle.kts
implementation("global.mattr.mobilecredential:verifier:5.2.0")
implementation("androidx.navigation:navigation-compose:2.9.0")
  • The verifier dependency version should match the version of the unzipped verifier-*version*.zip file you copied to the repo folder.
  • The required navigation-compose version may differ based on your version of the IDE, Gradle, and other project dependencies.
  1. Sync the project with Gradle files.

  2. Open the Build tab and select Sync to make sure that the project has synced successfully.

    Synced successfully

Step 3: Run the application

  1. Connect a debuggable mobile device to your machine.

  2. Build and run the app on the connected mobile device.

    The app should launch with a “Hello, Android!” text displayed:

    Blank app

Step 1: Access the tutorial codebase

  1. Access the tutorial starter codebase by either:

    • Cloning the MATTR sample-apps repository:

      Clone the repository
      git clone https://github.com/mattrglobal/sample-apps.git

      or

    • Downloading just the starter directory using the download-directory.github.io utility.

  2. 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

  1. 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.

  1. 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:

app.config.ts
    "./withMobileCredentialAndroidVerifierSdk",
    [
      "expo-build-properties",
      {
        android: {
          minSdkVersion: 24,
          compileSdkVersion: 35,
          targetSdkVersion: 34,
          kotlinVersion: "2.0.21",
        },
      },
    ],
    [
      "expo-camera",
      {
        cameraPermission: "Allow $(PRODUCT_NAME) to access your camera",
      },
    ],

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

  1. 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/
  1. 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!

Initialize the SDK

Tutorial Step 2

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.

Step 1: Create the application structure

  1. Open the ContentView file in your new project and replace any existing code with the following:

    ContentView
    import SwiftUI
    // Initialize SDK - Step 2.1: import MobileCredentialVerifierSDK
    
    struct ContentView: View {
        @ObservedObject var viewModel: VerifierViewModel = VerifierViewModel()
    
        var body: some View {
            NavigationStack(path: $viewModel.navigationPath) {
                VStack {
                    Button("Certificate Management") {
                        viewModel.navigationPath.append(NavigationState.certificateManagement)
                    }
                    .padding()
    
                    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 .certificateManagement:
                        certificateManagementView
                    case .scanQRCode:
                        codeScannerView
                    case .viewResponse:
                        presentationResponseView
                    }
                }
            }
        }
    
        // MARK: Verification Views
    
    
        var codeScannerView: some View {
        // Verify mDocs - Step 2.4: Create QRScannerView
            EmptyView()
        }
        // Manage Certificates - Step 1.2: Create CertificateManagementView
        var certificateManagementView: some View {
            EmptyView()
        }
        var presentationResponseView: some View {
        // Verify mDocs - Step 4.2: Create PresentationResponseView
            EmptyView()
        }
    }
    
    // MARK: VerifierViewModel
    
    final 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
    }
    
    // MARK: Proximity Presentation
    extension 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: - Navigation
    enum NavigationState: Hashable {
        case certificateManagement
        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/com/example/verifiertutorial/MainActivity.kt file in your project and replace any existing code with the following:

MainActivity.kt
package com.example.verifiertutorial

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.example.verifiertutorial.ui.theme.VerifierTutorialTheme
import global.mattr.mobilecredential.common.dto.MobileCredentialResponse
import global.mattr.mobilecredential.verifier.MobileCredentialVerifier
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Initialize SDK - Step 1.1: Initialize the SDK

        enableEdgeToEdge()
        setContent {
            VerifierTutorialTheme {
                val navController = rememberNavController()
                NavHost(
                    modifier = Modifier
                        .fillMaxSize()
                        .padding(vertical = 72.dp, horizontal = 8.dp),
                    startDestination = "home",
                    navController = navController,
                ) {
                    composable("home") {
                        HomeScreen(navController)
                    }
                    composable("certManagement") {
                        // Manage Certificates - Step 2.3: Add certificates management screen call
                    }
                    composable("scanOffer") {
                        // Verify mDocs - Step 1.7: Add "Scan Offer" screen call
                    }
                    composable("viewResponse") {
                        // Verify mDocs - Step 4.4: Add "View Response" screen call
                    }
                }
            }
        }
    }
}

@Composable
fun HomeScreen(navController: NavController) {
    Column(modifier = Modifier.fillMaxWidth()) {
        Button(
            modifier = Modifier.fillMaxWidth(),
            onClick = { navController.navigate("certManagement") }
        ) {
            Text("Certificate Management")
        }
        Button(
            modifier = Modifier.fillMaxWidth(),
            onClick = { navController.navigate("scanOffer") }
        ) {
            Text("Scan Presentation Offer")
        }
    }
}

// Verify mDocs - Step 2.2: Add shared data

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.

  1. Open the App.tsx file in your project and replace the existing code with the skeleton structure:

    App.tsx
    import {
        type MobileCredentialResponse,
        type TrustedIssuerCertificate,
        createProximityPresentationSession,
        getTrustedIssuerCertificates,
        initialize,
        sendProximityPresentationRequest,
        terminateProximityPresentationSession,
    } from "@mattrglobal/mobile-credential-verifier-react-native";
    // import { CertificateManagementModal } from "./CertificateManagementModal";
    // 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 messages
    const [isSDKInitialized, setIsSDKInitialized] = useState(false);
    const [loadingMessage, setLoadingMessage] = useState<string | false>(false);
    
    const [trustedCertificates, setTrustedCertificates] = useState<TrustedIssuerCertificate[]>([]);
    const [verificationResults, setVerificationResults] = useState<MobileCredentialResponse | null>(null);
    
    // Modal states
    const [showCertificateManagement, setShowCertificateManagement] = useState(false);
    const [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 function
    
    return (
        <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}>
                        {/* Manage Certificates - Step 1.2: Create Certificate Management Button */}
    
                        {/* Verify mDocs - Step 1.5: Create Scan QR Code Button */}
                    </View>
    
                {!isSDKInitialized && <Text style={styles.errorText}>SDK not initialized. Please restart the app.</Text>}
                {isSDKInitialized && trustedCertificates.length === 0 && (
                    <Text style={styles.errorText}>
                    No trusted issuer certificates added. Add certificates to verify mDocs.
                    </Text>
                )}
                </View>
            )}
    
            {/* Manage Certificates - Step: 2.6: Use CertificateManagementModal */}
    
            {/* 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.

Step 2: Initialize the MobileCredentialVerifier class

  1. 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
  2. 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
  3. 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:

    ContentView
        init() {
            do {
                mobileCredentialVerifier = MobileCredentialVerifier.shared
                try mobileCredentialVerifier.initialize()
            } catch {
                print(error.localizedDescription)
            }
        }
  4. Run the app to ensure it compiles successfully.

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.

  1. 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.

    MainActivity.kt
    lifecycleScope.launch {
        MobileCredentialVerifier.initialize(this@MainActivity)
    }
  2. Run the app to make sure it compiles properly.

  1. 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...");
          await initialize();
          setIsSDKInitialized(true);
    
          setLoadingMessage("Loading certificates...");
          const certificates = await getTrustedIssuerCertificates();
          if (certificates) {
            setTrustedCertificates(certificates);
          }
        } catch (error) {
          console.error("Failed to initialize SDK:", error);
          Alert.alert("Error", "Failed to initialize the verifier SDK");
        } finally {
          setLoadingMessage(false);
        }
      };
    
      initializeSDK();
    }, []);
  2. Run the app.

Manage certificates

Tutorial Step 3

Once the SDK is initialized, the next step is to build the capability for the application to manage certificates.

Tutorial Workflow

Every mDoc is signed by a series of certificates, referred to as a chain of trust. For your application to verify a presented mDoc it must validate it was signed using a root certificate (IACA) associated with a trusted issuer.

To enable this, your application should provide an interface for the user to manage (add, view and remove) certificates. We will achieve this by creating a new CertificateManagementView view and building certificate management capabilities into it.

Step 1: Create the Certificate Management view/screen

  1. Create a new file named CertificateManagementView and paste the following code in it:

    CertificateManagementView
    import SwiftUI
    import MobileCredentialVerifierSDK
    
    struct CertificateManagementView: View {
        @ObservedObject var viewModel = CertificateManagementViewModel()
        @State var certificateString = ""
    
        var body: some View {
            Form {
                Section(
                    header: Text("IACA Certificate").font(.headline),
                    footer: HStack {
                        Spacer()
                        Button("Add") {
                            viewModel.addCertificate(certificateString)
                        }
                        Spacer().frame(width: 30)
                        Button("Clear") {
                            certificateString = ""
                        }
                        .foregroundColor(.red)
                        .contentShape(Rectangle())
                        .frame(alignment: .trailing)
                    }
                ) {
                    TextField("IACA certificate string", text: $certificateString)
                }
    
                Section(
                    header: Text("Stored Certificates").font(.headline)
                ) {
                    certificateListView
                }
            }
            .navigationBarTitle("Certificate Setting")
            .onAppear {
                viewModel.getCertificates()
            }
        }
    
        // MARK: Certificate Management Views
        var certificateListView: some View {
        // Manage Certificates - Step 2.3: Display retrieved certificates
            EmptyView()
        }
    }
    
    final class CertificateManagementViewModel: ObservableObject {
        // Manage Certificates - Step 2.1: Add certificates and verifier variable
    
        func getCertificates() {
        // Manage Certificates - Step 2.2: Create getCertificates function
            print("This will fetch certificates from storage")
        }
    
        func addCertificate(_ certificate: String) {
        // Manage Certificates - Step 2.4: Create addCertificate function
            print("This will add certificate to storage and get a new list of certificates")
        }
        func removeCertificate(_ certificateID: String) {
        // Manage Certificates - Step 2.5: Create removeCertificate function
            print("This will remove certificates from storage and get a new list of certificates")
        }
    }

The CertificateManagementView file provides a basic user interface for displaying a list of certificates and controls for managing certificate storage. The CertificateManagementViewModel class contains the logic for adding, retrieving, and removing certificates.

  1. Return to the ContentView file and replace the EmptyView() under the // Manage certificates - Step 1.2: Create CertificateManagementView comment with a reference to the new view you created in the previous step:

    ContentView
        CertificateManagementView()
  2. Run the app and select the Certificate Management button. You should see a result similar to the following:

As the user selects the Certificate Management button they are navigated to the new CertificateManagement view where they can see controls that would enable them to add, view and remove certificates. We will build these capabilities into the controls in the next step.

  1. In your package, create a new file CertManagementScreen.kt.

    Cert management screen created

  2. Add the following code to the file to display the basic UI that enables the user to add, view and remove certificates:

    CertManagementScreen.kt
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.Row
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.fillMaxWidth
    import androidx.compose.foundation.layout.height
    import androidx.compose.foundation.layout.padding
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.items
    import androidx.compose.material.icons.Icons
    import androidx.compose.material.icons.filled.Delete
    import androidx.compose.material3.Card
    import androidx.compose.material3.Icon
    import androidx.compose.material3.IconButton
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Text
    import androidx.compose.material3.TextButton
    import androidx.compose.material3.TextField
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.LaunchedEffect
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.unit.dp
    import global.mattr.mobilecredential.common.dto.TrustedCertificate
    import global.mattr.mobilecredential.verifier.MobileCredentialVerifier
    
    @Composable
    fun CertManagementScreen() {
        Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
            var certText by remember { mutableStateOf("") }
            var storedCerts by remember { mutableStateOf(listOf<TrustedCertificate>()) }
            // Manage Certificates - Step 2.3: Load certificates when screen enters the composition
    
            Text("IACA CERT", style = MaterialTheme.typography.titleMedium)
            TextField(
                modifier = Modifier.fillMaxWidth(),
                value = certText,
                onValueChange = { certText = it },
                singleLine = true
            )
            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
                // Manage Certificates - Step 2.1: Add "Add certificate" button
    
                TextButton(onClick = { certText = "" }) { Text("Clear") }
            }
    
            Text("STORED CERTIFICATES", style = MaterialTheme.typography.titleMedium)
            LazyColumn(
                modifier = Modifier.fillMaxWidth(),
                verticalArrangement = Arrangement.spacedBy(6.dp)
            ) {
                items(storedCerts, key = { it.id }) { cert ->
                    Row(modifier = Modifier.fillMaxWidth()) {
                        Card(Modifier.weight(1f).height(85.dp)) {
                            Text(cert.pem, Modifier.padding(6.dp))
                        }
    
                        // Manage Certificates - Step 2.2: Add "Delete certificate" button
                    }
                }
            }
        }
    }
  3. Back in the MainActivity file, add the following code under the // Manage Certificates - Step 2.3: Add certificates management screen call comment to connect the created composable to the navigation graph:

    MainActivity.kt
    CertManagementScreen()
  4. Run the app and select the Certificate Management button. You should be navigated to the new certificate management screen, where you can see controls that would enable the user to add, view and remove certificates. We will build these capabilities into the controls in the next step.

The Certificate Management functionality is implemented as a separate modal component. Let's build it step by step:

  1. Create a new file named CertificateManagementModal.tsx and add the following code to it:

    CertificateManagementModal.tsx
    import {
        type TrustedIssuerCertificate,
        addTrustedIssuerCertificates,
        deleteTrustedIssuerCertificate,
        getTrustedIssuerCertificates,
    } from "@mattrglobal/mobile-credential-verifier-react-native";
    import { useState } from "react";
    import { Alert, Modal, SafeAreaView, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native";
    import { styles } from "./styles";
    
    interface CertificateManagementModalProps {
        visible: boolean;
        onClose: () => void;
        trustedCertificates: TrustedIssuerCertificate[];
        setTrustedCertificates: React.Dispatch<React.SetStateAction<TrustedIssuerCertificate[]>>;
    }
    
    export function CertificateManagementModal({
        visible,
        onClose,
        trustedCertificates,
        setTrustedCertificates,
    }: CertificateManagementModalProps) {
    
    const [certificateData, setCertificateData] = useState("");
    
    // Sample certificate for testing purposes - montcliff-dmv.mattrlabs.com IACA
    const sampleCertificate = `MIICYzCCAgmgAwIBAgIKXhjLoCkLWBxREDAKBggqhkjOPQQDAjA4MQswCQYDVQQG
    EwJBVTEpMCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0Ew
    HhcNMjQwMTE4MjMxNDE4WhcNMzQwMTE1MjMxNDE4WjA4MQswCQYDVQQGEwJBVTEp
    MCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0EwWTATBgcq
    hkjOPQIBBggqhkjOPQMBBwNCAASBnqobOh8baMW7mpSZaQMawj6wgM5e5nPd6HXp
    dB8eUVPlCMKribQ7XiiLU96rib/yQLH2k1CUeZmEjxoEi42xo4H6MIH3MBIGA1Ud
    EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRFZwEOI9yq
    232NG+OzNQzFKa/LxDAuBgNVHRIEJzAlhiNodHRwczovL21vbnRjbGlmZi1kbXYu
    bWF0dHJsYWJzLmNvbTCBgQYDVR0fBHoweDB2oHSgcoZwaHR0cHM6Ly9tb250Y2xp
    ZmYtZG12LnZpaS5hdTAxLm1hdHRyLmdsb2JhbC92Mi9jcmVkZW50aWFscy9tb2Jp
    bGUvaWFjYXMvMjk0YmExYmMtOTFhMS00MjJmLThhMTctY2IwODU0NWY0ODYwL2Ny
    bDAKBggqhkjOPQQDAgNIADBFAiAlZYQP95lGzVJfCykhcpCzpQ2LWE/AbjTGkcGI
    SNsu7gIhAJfP54a2hXz4YiQN4qJERlORjyL1Ru9M0/dtQppohFm6
    `;
    
    // Manage Certificates - Step 2.1: Create addCertificate function
    
    // Manage Certificates - Step 2.4: Create removeCertificate function
    
    return (
        <Modal visible={visible} animationType="slide" transparent={false}>
        <SafeAreaView style={styles.container}>
            <View style={styles.header}>
            <Text style={styles.title}>Certificate Management</Text>
            <TouchableOpacity onPress={onClose}>
                <Text style={styles.buttonText}>Close</Text>
            </TouchableOpacity>
            </View>
    
            <ScrollView style={styles.content}>
            {/* Manage Certificates - Step 2.2: Add new certificate form */}
    
            {/* Manage Certificates - Step 2.3: Display retrieved certificates */}
            </ScrollView>
        </SafeAreaView>
        </Modal>
    );
    }
  2. Return to your App.tsx file and add the following code under the Manage Certificates - Step 1.2: Create Certificate Management Button comment to create a button that opens the certificate management modal:

    App.tsx
    <TouchableOpacity
        style={styles.button}
        onPress={() => setShowCertificateManagement(true)}
    >
        <Text style={styles.buttonText}>Manage Certificates</Text>
    </TouchableOpacity>

Step 2: Add Certificate Management functionalities

Currently our Certificate Management view/screen has no functionalities. Let's fix this by adding the capabilities to add, view and remove certificates.

  1. Open the CertificateManagementView file and add the following code under the // Manage Certificates - Step 2.1: Add certificates variable comment to add a new certificates variable that will hold the certificates added to the application and a reference to a shared instance of MobileCredentialVerifier:

    CertificateManagementView
        @Published var certificates: [TrustedCertificate] = []
        let mobileCredentialVerifier = MobileCredentialVerifier.shared
  2. Replace the print statement under the // Manage Certificates - Step 2.2: Create getCertificates function comment to call the SDK getTrustedIssuerCertificates, retrieve all certificates and display them to the user:

    CertificateManagementView
            do {
                let fetchedCertificates = try mobileCredentialVerifier.getTrustedIssuerCertificates()
                certificates = fetchedCertificates
            } catch {
                print(error.localizedDescription)
            }
  3. Replace the EmptyView() under the // Manage Certificates - Step 2.3: Display retrieved certificates comment to iterate over retrieved certificates and display them to the user:

    CertificateManagementView
        ForEach(viewModel.certificates, id: \.id) { certificate in
            Text("\(certificate.pem)")
                .frame(maxHeight: 100)
                .swipeActions(edge: .trailing) {
                    Button(role:. destructive) {
                        viewModel.removeCertificate(certificate.id)
                    } label: {
                        Image(systemName: "trash")
                    }
                }
        }
  4. Replace the print statement under the // Manage Certificates - Step 2.4: Create addCertificate function comment to call the SDK addTrustedIssuerCertificates, accept a string parameter and use it to add a new certificate:

    CertificateManagementView
        Task { @MainActor in
            do {
                _ = try await mobileCredentialVerifier.addTrustedIssuerCertificates(certificates: [certificate])
                self.getCertificates()
            }
            catch {
                print(error.localizedDescription)
            }
        }
  5. Replace the print statement under the // Manage Certificates - Step 2.5: Create removeCertificate function to call the SDK deleteTrustedIssuerCertificate and remove a selected certificate:

    CertificateManagementView
        do {
            try mobileCredentialVerifier.deleteTrustedIssuerCertificate(certificateId: certificateID)
            // Refresh the certificates list after deletion
            self.getCertificates()
        } catch {
            print(error.localizedDescription)
        }
  6. Run the app and perform the following instructions:

    1. Select the Manage Certificates button.

    2. Copy and paste the following text into the IACA Certificate text box.

      MIICYzCCAgmgAwIBAgIKXhjLoCkLWBxREDAKBggqhkjOPQQDAjA4MQswCQYDVQQG
      EwJBVTEpMCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0Ew
      HhcNMjQwMTE4MjMxNDE4WhcNMzQwMTE1MjMxNDE4WjA4MQswCQYDVQQGEwJBVTEp
      MCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0EwWTATBgcq
      hkjOPQIBBggqhkjOPQMBBwNCAASBnqobOh8baMW7mpSZaQMawj6wgM5e5nPd6HXp
      dB8eUVPlCMKribQ7XiiLU96rib/yQLH2k1CUeZmEjxoEi42xo4H6MIH3MBIGA1Ud
      EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRFZwEOI9yq
      232NG+OzNQzFKa/LxDAuBgNVHRIEJzAlhiNodHRwczovL21vbnRjbGlmZi1kbXYu
      bWF0dHJsYWJzLmNvbTCBgQYDVR0fBHoweDB2oHSgcoZwaHR0cHM6Ly9tb250Y2xp
      ZmYtZG12LnZpaS5hdTAxLm1hdHRyLmdsb2JhbC92Mi9jcmVkZW50aWFscy9tb2Jp
      bGUvaWFjYXMvMjk0YmExYmMtOTFhMS00MjJmLThhMTctY2IwODU0NWY0ODYwL2Ny
      bDAKBggqhkjOPQQDAgNIADBFAiAlZYQP95lGzVJfCykhcpCzpQ2LWE/AbjTGkcGI
      SNsu7gIhAJfP54a2hXz4YiQN4qJERlORjyL1Ru9M0/dtQppohFm6

You will need to copy this text from the device you are using to display this tutorial and paste it in the device where you are running the built application. There are several ways to achieve this on iOS devices, one of them is setting up a Universal Clipboard.

  1. Select the Add button. The new certificate should appear in the STORED CERTIFICATES area.

  2. Swipe left across the new certificate and select the Delete icon to remove it.

You should get a result similar to the following:

  1. When the user selects the Certificate Management button they are navigated to the CertificateManagement view.
  2. When the user adds the text and selects the Add button a new certificate is added and displayed in the STORED CERTIFICATES area.
  3. When the user swipes left and selects the Delete icon the certificate is removed.

You have now built the capabilities required to manage certificates into your applications. Now we can proceed to building the capabilities to handle the actual presentation workflow.

  1. Open the CertManagementScreen.kt file and add the following code under the // Manage Certificates - Step 2.1: Add "Add certificate" button comment to add a button, that will be responsible for adding the certificate, entered in the text field above:

    CertManagementScreen.kt
    TextButton(
        onClick = {
            MobileCredentialVerifier.addTrustedIssuerCertificates(listOf(certText))
            storedCerts = MobileCredentialVerifier.getTrustedIssuerCertificates()
        }
    ) { Text("Add") }
  2. Add the following code under the // Manage Certificates - Step 2.2: Add "Delete certificate" button comment to add a button, that will delete the certificate, when pressed:

    CertManagementScreen.kt
    IconButton(
        onClick = {
            MobileCredentialVerifier.deleteTrustedIssuerCertificate(cert.id)
            storedCerts = MobileCredentialVerifier.getTrustedIssuerCertificates()
        },
        modifier = Modifier.align(Alignment.CenterVertically)
    ) {
        Icon(
            Icons.Default.Delete,
            contentDescription = "Delete Cert",
            tint = MaterialTheme.colorScheme.primary
        )
    }
  3. Add the following code under the // Manage Certificates - Step 2.3: Load certificates when screen enters the composition comment to enable loading the certificates from the local storage and showing them, when the screen enters the composition:

    CertManagementScreen.kt
    LaunchedEffect(Unit) {
        storedCerts = MobileCredentialVerifier.getTrustedIssuerCertificates()
    }
  4. Run the app and perform the following instructions:

    1. Select the Manage Certificates button.

    2. Copy and paste the following text into the IACA Certificate text box.

      MIICYzCCAgmgAwIBAgIKXhjLoCkLWBxREDAKBggqhkjOPQQDAjA4MQswCQYDVQQG
      EwJBVTEpMCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0Ew
      HhcNMjQwMTE4MjMxNDE4WhcNMzQwMTE1MjMxNDE4WjA4MQswCQYDVQQGEwJBVTEp
      MCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0EwWTATBgcq
      hkjOPQIBBggqhkjOPQMBBwNCAASBnqobOh8baMW7mpSZaQMawj6wgM5e5nPd6HXp
      dB8eUVPlCMKribQ7XiiLU96rib/yQLH2k1CUeZmEjxoEi42xo4H6MIH3MBIGA1Ud
      EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRFZwEOI9yq
      232NG+OzNQzFKa/LxDAuBgNVHRIEJzAlhiNodHRwczovL21vbnRjbGlmZi1kbXYu
      bWF0dHJsYWJzLmNvbTCBgQYDVR0fBHoweDB2oHSgcoZwaHR0cHM6Ly9tb250Y2xp
      ZmYtZG12LnZpaS5hdTAxLm1hdHRyLmdsb2JhbC92Mi9jcmVkZW50aWFscy9tb2Jp
      bGUvaWFjYXMvMjk0YmExYmMtOTFhMS00MjJmLThhMTctY2IwODU0NWY0ODYwL2Ny
      bDAKBggqhkjOPQQDAgNIADBFAiAlZYQP95lGzVJfCykhcpCzpQ2LWE/AbjTGkcGI
      SNsu7gIhAJfP54a2hXz4YiQN4qJERlORjyL1Ru9M0/dtQppohFm6

You will need to copy this text from the device you are using to display this tutorial and paste it in the device where you are running the built application. This can be achieved by navigating to this tutorial on your testing mobile device.

  1. Select the Add button. The new certificate should appear in the STORED CERTIFICATES area.

  2. Press Delete icon to remove the certificate.

You should get a result similar to the following:

  1. When the user selects the Certificate Management button they are navigated to the CertManagementScreen.
  2. When the user adds the text and selects the Add button a new certificate is added and displayed in the STORED CERTIFICATES area.
  3. When the user selects the Delete icon the certificate is removed.
  1. Return to the CertificateManagementModal.tsx file and paste the following code under the // Manage Certificates - Step 2.1: Create addCertificate function comment to implement the addCertificate function:

    CertificateManagementModal.tsx
    const addCertificate = async () => {
        if (!certificateData.trim()) {
            Alert.alert("Error", "Please enter certificate data");
            return;
        }
    
        try {
            const result = await addTrustedIssuerCertificates([certificateData]);
    
            if (result.isErr()) {
                throw new Error(result.error.message);
            }
    
            // Reload certificates after adding
            const certificates = await getTrustedIssuerCertificates();
            if (certificates.length > 0) {
                setTrustedCertificates(certificates);
            }
    
            // Clear input fields
            setCertificateData("");
    
            Alert.alert("Success", "Certificate added successfully");
        } catch (error) {
            console.error("Error adding certificate:", error);
            Alert.alert("Error", "Failed to add certificate");
        }
    };
  2. Paste the following code under the // Manage Certificates - Step 2.2: Add new certificate form comment to add a certificate input form:

    CertificateManagementModal.tsx
    <View style={styles.card}>
        <Text style={styles.cardTitle}>Add New Certificate</Text>
        <Text style={[styles.smallText, styles.grayColor]}>Certificate Data (Base64)</Text>
        <TextInput
            spellCheck={false}
            style={[styles.formField, styles.expandingInput]}
            multiline
            placeholder="Paste certificate data here..."
            value={certificateData}
            onChangeText={setCertificateData}
        />
    
        <View style={styles.buttonRow}>
            <TouchableOpacity style={styles.button} onPress={addCertificate}>
                <Text style={styles.buttonText}>Add Certificate</Text>
            </TouchableOpacity>
    
            <TouchableOpacity
                style={[styles.button, styles.secondaryButton]}
                onPress={() => setCertificateData(sampleCertificate)}
            >
                <Text style={[styles.buttonText, styles.secondaryButtonText]}>Load Example</Text>
            </TouchableOpacity>
        </View>
    </View>
  3. Paste the following code under the // Manage Certificates - Step 2.3: Display retrieved certificates comment to display the trusted certificates list:

    CertificateManagementModal.tsx
    <View style={styles.card}>
        <Text style={styles.cardTitle}>Trusted Certificates ({trustedCertificates.length})</Text>
    
        {trustedCertificates.map((certificate: TrustedIssuerCertificate) => (
          <View key={certificate.id} style={styles.listItem}>
            <View style={styles.listItemContent}>
              <Text style={styles.listItemTitle}>{certificate.commonName}</Text>
              <Text style={[styles.smallText, styles.grayColor]}>ID: {certificate.id.substring(0, 20)}...</Text>
            </View>
    
            {/* Manage Certificates - Step 2.5: Remove certificate button */}
          </View>
        ))}
    </View>
  4. Paste the following code under the // Manage Certificates - Step 2.4: Create removeCertificate function comment to implement the removeCertificate function:

    CertificateManagementModal.tsx
    const removeCertificate = async (id: string) => {
        try {
            await deleteTrustedIssuerCertificate(id);
            setTrustedCertificates(
                trustedCertificates.filter((certificate: TrustedIssuerCertificate) => certificate.id !== id)
            );
            Alert.alert("Success", "Certificate removed successfully");
        } catch (error) {
            console.error("Error removing certificate:", error);
            Alert.alert("Error", "Failed to remove certificate");
        }
    };
  5. Paste the following code under the // Manage Certificates - Step 2.5: Remove certificate button comment to add a remove button to each certificate:

    CertificateManagementModal.tsx
    <TouchableOpacity style={styles.dangerButton} onPress={() => removeCertificate(certificate.id)}>
        <Text style={styles.buttonText}>Remove</Text>
    </TouchableOpacity>
  6. Navigate to the App.tsx file and uncomment the following import at the top of the file:

    App.tsx
    import { CertificateManagementModal } from "./CertificateManagementModal";
  7. Paste the following code under the // Manage Certificates - Step 2.6: Use CertificateManagementModal comment to integrate the certificates modal into the main app:

    App.tsx
    <CertificateManagementModal
         visible={showCertificateManagement}
         onClose={() => setShowCertificateManagement(false)}
         trustedCertificates={trustedCertificates}
         setTrustedCertificates={setTrustedCertificates}
       />

You have successfully built the certificate management capabilities for your verifier application. You can now add and remove trusted issuer certificates and view the stored certificate list.

Now you can proceed to building the capabilities to handle the actual presentation workflow.

Verify mDocs

Tutorial Step 4

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:

Tutorial Workflow

To achieve this, your application must be able to:

  1. Create a presentation request that defines the information required for verification.
  2. 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.
  3. Your verifier application then uses this secure connection to send a presentation request to which the holder wallet application responds with a presentation response.
  4. 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.

  1. 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:

    ContentView
        let mobileCredentialRequest = MobileCredentialRequest(
            docType: "org.iso.18013.5.1.mDL",
            namespaces: [
                "org.iso.18013.5.1":  [
                    "family_name": false,
                    "given_name": false,
                    "birth_date": false
                ]
            ]
        )

    This object details:

    • 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.

  1. 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

Tutorial Workflow

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.

Tutorial Workflow

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.

  1. Add camera usage permissions to the app target:

Camera permissions

  1. Add the CodeScanner library via Swift Package Manager.

Code scanner package

  1. Create a new swift file named QRScannerView and add the following code into it to implement the QR scanning capability:

    QRScannerView
    import SwiftUI
    import CodeScanner
    
    struct 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)
                }
            }
        }
    }
  2. 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)
            }
        )
  3. 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

  1. 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.

  2. 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:

    ContentView
     mobileCredentialVerifier.createProximityPresentationSession(encodedDeviceEngagementString: deviceEngagementString, listener: self)
  3. 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 = []
            }
        }

    This function now implements the following logic:

    1. Navigate to the viewResponse screen.
    2. Send a proximity presentation request using the SDK's requestMobileCredentials function.
    3. Store the wallet response in the deviceResponse variable. This includes the verification results of any credentials included in the response.
    4. Store the verification results in the receivedDocuments variable.
    5. Terminate the presentation session once the response is received.

Step 4: Display verification results

  1. Create a new file named DocumentView and add the following code to display available verification results:

    DocumentView
    import MobileCredentialVerifierSDK
    import SwiftUI
    
        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.
  2. 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:

    ContentView
            ZStack {
            if viewModel.receivedDocuments.isEmpty {
                VStack(spacing: 40) {
                    Text("Waiting for response...")
                        .font(.title)
                    ProgressView()
                        .progressViewStyle(.circular)
                        .scaleEffect(2)
                }
            } else {
                ScrollView {
                    ForEach(viewModel.receivedDocuments, id: \.docType) { doc in
                        DocumentView(viewModel: DocumentViewModel(from: doc))
                            .padding(10)
                    }
                }
            }
        }

Step 1: Create a screen for scanning the credential offer

Tutorial Workflow

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.

Tutorial Workflow

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.

  1. Add dependencies to your app/build.gradle.kts:

    app/build.gradle.kts
    implementation("com.google.accompanist:accompanist-permissions:0.36.0")
    implementation("com.journeyapps:zxing-android-embedded:4.3.0")
  2. Sync project with Gradle files.

  3. In your package, create a new file called ScanOfferScreen.kt and add the following code:

    ScanOfferScreen.kt
    import android.app.Activity
    import android.Manifest
    import android.widget.Toast
    import androidx.activity.compose.rememberLauncherForActivityResult
    import androidx.activity.result.contract.ActivityResultContracts
    import androidx.compose.foundation.layout.Box
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.material3.CircularProgressIndicator
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.DisposableEffect
    import androidx.compose.runtime.LaunchedEffect
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.viewinterop.AndroidView
    import androidx.navigation.NavController
    import com.google.accompanist.permissions.ExperimentalPermissionsApi
    import com.google.accompanist.permissions.isGranted
    import com.google.accompanist.permissions.rememberPermissionState
    import com.journeyapps.barcodescanner.BarcodeCallback
    import com.journeyapps.barcodescanner.DecoratedBarcodeView
    import global.mattr.mobilecredential.common.deviceretrieval.devicerequest.DataElements
    import global.mattr.mobilecredential.common.deviceretrieval.devicerequest.NameSpaces
    import global.mattr.mobilecredential.common.dto.MobileCredentialRequest
    import global.mattr.mobilecredential.common.dto.MobileCredentialResponse
    import global.mattr.mobilecredential.verifier.MobileCredentialVerifier
    import global.mattr.mobilecredential.verifier.ProximityPresentationSessionListener
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.launch
    import kotlinx.coroutines.suspendCancellableCoroutine
    import kotlin.coroutines.Continuation
    import kotlin.coroutines.resume
    import kotlin.coroutines.resumeWithException
    
    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun ScanOfferScreen(activity: Activity, navController: NavController) {
        // Verify mDocs - Step 1.6: Add permission request logic
    }
    
    // Verify mDocs - Step 1.5: Add screen content
    
    // Verify mDocs - Step 1.4: Add QR scan callback
    
    // Verify mDocs - Step 3.1: Create session listener
    
    // Verify mDocs - Step 2.1: Create a sample request
  4. In the ScanOfferScreen.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):

    ScanOfferScreen.kt
     private fun onQrScanned(
         activity: Activity,
         deviceEngagement: String,
         coroutineScope: CoroutineScope,
         navController: NavController
     ) {
         coroutineScope.launch {
             // Verify mDocs - Step 3.2: Create session
    
             // Verify mDocs - Step 4.1: Handle response
         }
     }
  5. Add the following code under the // Verify mDocs - Step 1.5: Add screen content comment to define the main UI of the screen:

    ScanOfferScreen.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.

  6. 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:

    ScanOfferScreen.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)
  7. Back in the MainActivity file, add the following code under the // Verify mDocs - Step 1.7: Add "Scan Offer" screen call to connect the created screen to the navigation graph:

    MainActivity.kt
    ScanOfferScreen(this@MainActivity, navController)
  8. 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.

  1. In the ScanOfferScreen.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:

    ScanOfferScreen.kt
     private val sampleMdocRequest = MobileCredentialRequest(
         docType = "org.iso.18013.5.1.mDL",
         namespaces = NameSpaces(
             mapOf(
                 "org.iso.18013.5.1" to DataElements(
                     listOf("given_name", "family_name", "birth_date").associateWith { false }
                 )
             )
         )
     )

    This object details:

    • 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.

  1. 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

  1. In ScanOfferScreen.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:

    ScanOfferScreen.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) }
        }
    }
  2. Add the following code under the // Verify mDocs - Step 3.2: Create session to create proximity presentation session and register a session listener:

    ScanOfferScreen.kt
    SharedData.credentialResponse = try {
        suspendCancellableCoroutine { continuation: Continuation<MobileCredentialResponse> ->
            val sessionListener = SessionListener(coroutineScope, continuation)
    
            MobileCredentialVerifier
                .createProximityPresentationSession(activity, deviceEngagement, sessionListener)
        }
    } catch (e: Exception) {
        Toast.makeText(activity, "Failed to request credentials", Toast.LENGTH_SHORT).show()
        null
    }

    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.

  3. Add the following code under the // Verify mDocs - Step 3.3: Request credentials to request the mobile credentials:

    ScanOfferScreen.kt
     try {
         val response = MobileCredentialVerifier.sendProximityPresentationRequest(
             listOf(sampleMdocRequest),
             skipStatusCheck = true
         )
         MobileCredentialVerifier.terminateProximityPresentationSession()
         continuation.resume(response)
     } catch (e: Exception) {
         continuation.resumeWithException(e)
     }

The resulting code:

  1. Establishes a secure connection with the wallet application by calling createProximityPresentationSession.
  2. Calls sendProximityPresentationRequest function as soon as the session is established. The function accepts a list of MobileCredentialRequests , sends the requests to the wallet application, receives a response from the wallet application, and verifies any mDocs included in the response.
  3. Stores the response in the SharedData.credentialResponse value.
  4. 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

  1. 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:

    ScanOfferScreen.kt
    SharedData.credentialResponse?.let {
       navController.navigate("viewResponse") { popUpTo("home") }
    }
  2. Create a new file named ViewResponseScreen.kt that will be used to display the response to the verifier application user.

  3. Copy and paste the following code into the new file:

    ViewResponseScreen.kt
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Box
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.ColumnScope
    import androidx.compose.foundation.layout.Row
    import androidx.compose.foundation.layout.Spacer
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.fillMaxWidth
    import androidx.compose.foundation.layout.padding
    import androidx.compose.foundation.rememberScrollState
    import androidx.compose.foundation.verticalScroll
    import androidx.compose.material3.Card
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.graphics.Color
    import androidx.compose.ui.text.style.TextOverflow
    import androidx.compose.ui.unit.dp
    import global.mattr.mobilecredential.common.deviceretrieval.deviceresponse.DataElementIdentifier
    import global.mattr.mobilecredential.common.deviceretrieval.deviceresponse.NameSpace
    import global.mattr.mobilecredential.common.dto.MobileCredentialElement
    
    @Composable
    fun ViewResponseScreen() {
        // Verify mDocs - Step 4.5: Define content
    }
    
    // Verify mDocs - Step 4.8: Display claims
    
    // Verify mDocs - Step 4.7: Map a claim or an error to string
  4. 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()
  5. 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.

  1. 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))
    }
  2. 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.

  1. 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:

    ViewResponseScreen.kt
    @Composable
    private fun ColumnScope.Claims(
        title: String,
        claims: Map<NameSpace, Map<DataElementIdentifier, Any>>?
    ) {
        Text(
            title,
            modifier = Modifier
                .fillMaxWidth()
                .align(Alignment.CenterHorizontally),
            style = MaterialTheme.typography.titleLarge
        )
        claims?.forEach { (namespace, claims) ->
            Card {
                Column(Modifier.padding(6.dp)) {
                    Text(
                        namespace,
                        style = MaterialTheme.typography.titleMedium,
                        modifier = Modifier.padding(vertical = 4.dp)
                    )
                    claims.forEach { (name, value) ->
                        Row {
                            Text(name,
                                Modifier
                                    .weight(1f)
                                    .padding(end = 4.dp))
                            Text(value.claimToUiString(), overflow = TextOverflow.Ellipsis)
                        }
                    }
                }
            }
        } ?: Text("Nothing here")
    }
  2. 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:

    ViewResponseScreen.kt
    Claims("Received claims", credential.claims)
    Spacer(Modifier.padding(8.dp))
    Claims("Failed claims", credential.claimErrors)
  3. 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.titleLarge
    if (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

  1. 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.
  1. 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:
App.tsx
const handleQRCodeDetected = async (qrData: string) => {
    try {
      setLoadingMessage("Establishing secure connection...");

      await createProximityPresentationSession({
        deviceEngagement: qrData,
        onEstablished: async () => {
          console.log("Session established successfully");
          setLoadingMessage("Requesting verification data...");
          try {
            const response = await sendProximityPresentationRequest({
              mobileCredentialRequests: [
                {
                  docType: "org.iso.18013.5.1.mDL",
                  namespaces: {
                    "org.iso.18013.5.1": {
                      family_name: false,
                      given_name: false,
                      birth_date: false,
                    },
                  },
                },
              ],
            });

            if (response.isErr()) {
              throw new Error(`Failed to verify presentation: ${response.error.message}`);
            }

            setLoadingMessage("Verifying credentials...");
            setVerificationResults(response.value);
            setShowVerificationResults(true);
            await terminateProximityPresentationSession();
          } catch (error) {
            console.error("Error during presentation request:", error);
            Alert.alert("Error", "Failed to verify mDocs");
            await terminateProximityPresentationSession();
          } finally {
            setLoadingMessage(false);
          }
        },
        onTerminated: () => {
          console.log("Session terminated");
          setLoadingMessage(false);
        },
        onError: (error) => {
          console.error("Session error:", JSON.stringify(error, null, 2));
          Alert.alert(
            "Error",
            `Session failed: ${error.message || JSON.stringify(error)}`,
          );
          setLoadingMessage(false);
        },
      });

      console.log(
        "createProximityPresentationSession call completed (waiting for callbacks)",
      );
    } catch (error) {
      console.error("Error during QR code processing:", error);
      Alert.alert(
        "Error",
        `Failed to process QR code: ${error instanceof Error ? error.message : String(error)}`,
      );
      setLoadingMessage(false);
    }
  };

This function requests an mDL (mobile driver's license) credential with specific data elements: family_name, given_name, and birth_date.

  1. Uncomment the QRScannerModal import at the top of the file to integrate the QR Scanner modal:
App.tsx
import { QRScannerModal } from "./QRScannerModal";
  1. Paste the following code under the // Verify mDocs - Step 1.4: Use QRScannerModal comment to render the QR Scanner modal component in your app:
App.tsx
<QRScannerModal
    visible={isScanning}
    onClose={() => setIsScanning(false)}
    permission={permission}
    requestPermission={requestPermission}
    onQRCodeDetected={handleQRCodeDetected}
/>
  1. Add the following code under the Verify mDocs - Step 1.5: Create Scan QR Code Button comment to create a button that opens the QR Scanner modal:
App.tsx
<TouchableOpacity
style={[styles.button, styles.secondaryButton]}
onPress={() => setIsScanning(true)}
>
    <Text style={[styles.buttonText, styles.secondaryButtonText]}>
        Scan QR Code
    </Text>
</TouchableOpacity>

Step 2: Display verification results

  1. 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.
VerificationResultsModal.tsx
import type { MobileCredentialResponse } from "@mattrglobal/mobile-credential-verifier-react-native";
import { Modal, SafeAreaView, ScrollView, Text, TouchableOpacity, View } from "react-native";
import { styles } from "./styles";

interface VerificationResultsModalProps {
  visible: boolean;
  onClose: () => void;
  verificationResults: MobileCredentialResponse | null;
}

export function VerificationResultsModal({ visible, onClose, verificationResults }: VerificationResultsModalProps) {
  if (!visible || !verificationResults) return null;

  // Helper function to render different claim value types
  function renderClaimValue(claim: any): string {
    if (!claim) return "undefined";

    if (claim.type === "array" || claim.type === "object") {
      return JSON.stringify(claim.value);
    }

    return String(claim.value);
  }

  return (
    <Modal visible={visible} animationType="slide" transparent={false}>
      <SafeAreaView style={styles.container}>
        <View style={styles.header}>
          <Text style={styles.title}>Verification Results</Text>
          <TouchableOpacity onPress={onClose}>
            <Text style={styles.buttonText}>Close</Text>
          </TouchableOpacity>
        </View>

        <ScrollView style={styles.content}>
          {verificationResults.credentials && verificationResults.credentials.length > 0 ? (
            <View>
              {/* Basic verification status */}
              <View style={[styles.center, styles.marginBottom]}>
                <Text
                  style={
                    verificationResults.credentials[0].verificationResult?.verified
                      ? styles.verificationSuccess
                      : styles.verificationFailed
                  }
                >
                  {verificationResults.credentials[0].verificationResult?.verified
                    ? "✓ Verified"
                    : "✗ Verification Failed"}
                </Text>
                <Text style={styles.verificationSubtext}>{verificationResults.credentials[0].docType}</Text>
              </View>

              {/* Display the raw credential data */}
              {verificationResults.credentials.map((credential, credIndex) => (
                <View key={`credential-${credIndex}`}>
                  {/* Claims data */}
                  {credential.claims &&
                    Object.keys(credential.claims).map((namespace, nsIndex) => (
                      <View key={`namespace-${nsIndex}`} style={styles.marginBottom}>
                        <Text style={styles.cardTitle}>{namespace}</Text>
                        <View style={styles.card}>
                          {credential.claims &&
                            Object.entries(credential.claims[namespace]).map(([key, value], idx) => (
                              <View key={`${namespace}-${key}-${idx}`} style={styles.listItem}>
                                <Text>{key}:</Text>
                                <Text>{renderClaimValue(value)}</Text>
                              </View>
                            ))}
                        </View>
                      </View>
                    ))}

                  {/* Error information */}
                  {!credential.verificationResult?.verified && credential.verificationResult?.reason && (
                    <View style={styles.card}>
                      <Text style={styles.listItemTitle}>Verification Failed:</Text>
                      <Text>Type: {credential.verificationResult.reason.type}</Text>
                      <Text>Message: {credential.verificationResult.reason.message}</Text>
                    </View>
                  )}

                  {/* Claim errors if any */}
                  {credential.claimErrors && Object.keys(credential.claimErrors).length > 0 && (
                    <View style={styles.marginBottom}>
                      <Text style={styles.cardTitle}>Claim Errors</Text>
                      <View style={styles.card}>
                        {Object.entries(credential.claimErrors).map(([namespace, errors]) =>
                          Object.entries(errors).map(([elementId, errorCode]) => (
                            <View key={`error-${namespace}-${elementId}`} style={styles.listItem}>
                              <Text>
                                {namespace}.{elementId}:
                              </Text>
                              <Text style={styles.errorColor}>Error: {errorCode}</Text>
                            </View>
                          ))
                        )}
                      </View>
                    </View>
                  )}
                </View>
              ))}
            </View>
          ) : (
            <View style={styles.card}>
              <Text style={[styles.centeredText, styles.grayColor]}>No data available</Text>
            </View>
          )}
        </ScrollView>
      </SafeAreaView>
    </Modal>
  );
}
  1. Return to your App.tsx file and uncomment the VerificationResultsModal import to integrate the verification results modal:
App.tsx
import { VerificationResultsModal } from "./VerificationResultsModal";
  1. Add the following code under the // Verify mDocs - Step 2.3: Use VerificationResultModal comment to display the verification results modal when verification is complete:
App.tsx
<VerificationResultsModal
    visible={showVerificationResults}
    onClose={() => setShowVerificationResults(false)}
    verificationResults={verificationResults}
/>

Test the end-to-end workflow

Tutorial Step 5

  1. Run the app.

  2. Select the Certificate Management button.

  3. Copy and paste the following text into the IACA Certificate text box.

    MIICYzCCAgmgAwIBAgIKXhjLoCkLWBxREDAKBggqhkjOPQQDAjA4MQswCQYDVQQG
    EwJBVTEpMCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0Ew
    HhcNMjQwMTE4MjMxNDE4WhcNMzQwMTE1MjMxNDE4WjA4MQswCQYDVQQGEwJBVTEp
    MCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0EwWTATBgcq
    hkjOPQIBBggqhkjOPQMBBwNCAASBnqobOh8baMW7mpSZaQMawj6wgM5e5nPd6HXp
    dB8eUVPlCMKribQ7XiiLU96rib/yQLH2k1CUeZmEjxoEi42xo4H6MIH3MBIGA1Ud
    EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRFZwEOI9yq
    232NG+OzNQzFKa/LxDAuBgNVHRIEJzAlhiNodHRwczovL21vbnRjbGlmZi1kbXYu
    bWF0dHJsYWJzLmNvbTCBgQYDVR0fBHoweDB2oHSgcoZwaHR0cHM6Ly9tb250Y2xp
    ZmYtZG12LnZpaS5hdTAxLm1hdHRyLmdsb2JhbC92Mi9jcmVkZW50aWFscy9tb2Jp
    bGUvaWFjYXMvMjk0YmExYmMtOTFhMS00MjJmLThhMTctY2IwODU0NWY0ODYwL2Ny
    bDAKBggqhkjOPQQDAgNIADBFAiAlZYQP95lGzVJfCykhcpCzpQ2LWE/AbjTGkcGI
    SNsu7gIhAJfP54a2hXz4YiQN4qJERlORjyL1Ru9M0/dtQppohFm6

You will need to copy this text from the device you are using to display this tutorial and paste it in the device where you are running the built application. There are several ways to achieve this on iOS devices, one of them is setting up a Universal Clipboard. For Android devices, you can simply visit the tutorial page on the device where you are running the built application and copy the text from there.

  1. Select the Add button.
  2. Return to the app main screen.
  3. Open your holder testing device and launch the GO Hold example app.
  4. Select the Wallet button.
  5. Locate the mDoc claimed as part of the prerequisites for this tutorial and select the share button to display a QR code.
  6. Use your verifier testing device and select the Scan QR Code button.
  7. Use the verifier testing device to scan the QR code displayed on the holder testing device.
  8. Use the holder testing device to consent to sharing the information with the verifier.
  9. Use the verifier testing device and select the View response button.

You should see a result similar to the following:

  1. The verifier app user adds a new certificate to the app. This is the certificate associated with the issuer of the mDoc we are about to verify.
  2. The wallet app user creates a QR code to initiate the proximity presentation workflow.
  3. The verifier app scans the QR code, establishes a secure connection and sends a presentation request.
  4. The wallet app user reviews the presentation request and agrees to share matching mDocs with the verifier.
  5. The verifier app receives and verifies the mDocs included in the presentation response.
  6. The verifier app user views the verification results.

Congratulations! Your verifier application can now verify mDocs presented via a proximity presentation workflow, as per ISO/IEC 18013-5:2021.

Summary

You have just used the mDocs Verifier SDKs to build an application that can verify an mDoc presented via a proximity workflow as per ISO/IEC 18013-5:2021:

Tutorial Workflow

This was achieved by building the following capabilities into the application:

  • Initialize the SDK, so that your application can use its functions and classes.
  • Manage certificates, which enable your application to verify mDocs that were issued by trusted issuers.
  • Scan a QR code presented by a wallet application and establish a secure communication channel.
  • Send presentation requests to the wallet application, receive a presentation response and verify its content.
  • Display the results to the verifier app user.

What's next?

  • You can check out SDKs reference documentation to learn more about available functions and classes:
  • You can implement NFC based device engagement capabilities (currently supported by the Android Verifier SDK and the React Native SDK for Android platforms only).

How would you rate this page?