light-mode-image
Learn
Mobile

Learn how to build an application that can verify an mDoc from another app on the same device

Overview

In this tutorial you will use the mDocs Mobile Verifier SDKs to build an application that can verify an mDoc presented from a different application on the same device via OID4VP, as defined in ISO 18013-7 Annex B.

Remote mobile app verification overview

  1. A relying party uses the Mobile Verifier SDK to embed a remote verification workflow into a mobile application.
  2. When a user interacts with the mobile application, a matching wallet application installed on their mobile device is invoked to request an mDoc for verification.
  3. The user consents to sharing the requested information.
  4. The user's wallet application shares the matching mDoc with the MATTR VII tenant configured by the Mobile Verifier SDK to perform the verification workflow.
  5. The MATTR VII tenant performs the required checks and returns the verification results via the Mobile Verifier SDK to the verifier application.
  6. The user journey continues based on the verification results.

The result will look something like this:

Similar to the iOS and Android videos, but with a single app running on both platforms.

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.
  • Request an mDoc for verification from a compliant wallet application.
  • Handle the redirect from the wallet application.
  • Display the verification results.

Prerequisites

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

Prior knowledge

  • The remote verification workflow described in this tutorial is based on the OID4VP specification and the ISO 18013-7 standard. If you are unfamiliar with these, refer to the following resources for more information:

  • We assume you have experience developing applications in the relevant programming languages and frameworks (Swift for iOS and Kotlin for Android).

Assets

  • Use the Get Started form to request a trial of MATTR verification capabilities. You will receive access to the following resources:
    • MATTR Pi mDocs Verifier SDK for your chosen platform (iOS, Android, or React Native).
    • MATTR VII tenant.
  • As part of your MATTR Pi SDK onboarding process you will be provided with access to the following resources:
    • ZIP file which includes the required framework: (MobileCredentialVerifierSDK.xcframework.zip).
    • Sample Verifier app: You can use this app for reference as you work through this tutorial.

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

  • Use the Get Started form to request a trial of MATTR verification capabilities. You will receive access to the following resources:
    • MATTR Pi mDocs Verifier SDK for your chosen platform (iOS, Android, or React Native).
    • MATTR VII tenant.
  • As part of your MATTR Pi SDK onboarding process you will be provided with access to the following resources:
    • A ZIP file that includes the required library (mobile-credential-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 latest version of the Android mDocs Verifier SDK.

  • Use the Get Started form to request a trial of MATTR verification capabilities. You will receive access to the following resources:
    • MATTR Pi mDocs Verifier SDK for your chosen platform (iOS, Android, or React Native).
    • MATTR VII tenant.
  • Access to the @mattrglobal/mobile-credential-verifier-react-native npm package, provided as part of your MATTR Pi SDK onboarding process.
  • Sample Verifier app: This tutorial is accompanied by a sample app you can use for reference as you work through it.

This tutorial is only meant to be used with the latest 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.

Testing device

You will need a mobile device to test the workflow with.

Supported iOS device to run the built Verifier application on, setup with:

  • Available internet connection
  • A wallet application that can present an mDoc remotely as per ISO/IEC 18013-7 and OID4VP. We recommend using the sample holder app you can build with our Holder SDK quickstart guide.
  • Use your testing wallet application to claim an mDoc by scanning the following QR code:
QR code

Supported Android device to run the built Verifier application on, setup with:

  • Available internet connection
  • A wallet application that can present an mDoc remotely as per ISO/IEC 18013-7 and OID4VP. We recommend using the sample holder app you can build with our Holder SDK quickstart guide.
  • Use your testing wallet application to claim an mDoc by scanning the following QR code:
QR code

A supported iOS or Android device to run the built Verifier application on, setup with:

  • Available internet connection
  • A wallet application that can present an mDoc remotely as per ISO/IEC 18013-7 and OID4VP. We recommend using the sample holder app you can build with our Holder SDK quickstart guide.
  • Use your testing wallet application to claim an mDoc by scanning the following QR code:
QR code

React Native apps built with Expo development builds must run on a physical device, not a simulator or emulator, to complete the app-to-app presentation flow with a wallet installed on the same device.

Got everything? Let's get going!

Overview

The following diagram depicts the workflow you will build in this tutorial:

  1. The user triggers the workflow by interacting with the verifier application.
  2. The verifier application uses the embedded Mobile Verifier SDK capabilities to start a presentation-based verification session with the configured MATTR VII tenant.
  3. The MATTR VII tenant responds with a link to invoke a matching wallet application.
  4. The verifier application uses the link to invoke a matching wallet application using a redirect.
  5. The wallet application makes a request to the MATTR VII tenant to retrieve a request object, defining what information is requested for verification.
  6. The MATTR VII tenant returns the request object to the wallet application.
  7. The wallet application (upon user consent) returns an authorization response to the MATTR VII tenant, which includes the information required for verification.
  8. The MATTR VII tenant returns the verification results to the verifier application.
  9. The verifier application surfaces the verification results to the user and the interaction continues.

You will build this workflow in two parts:

  1. Part 1: Setup the MATTR VII Verifier tenant.
  2. Part 2: Build a mobile application with mDocs verification capabilities.

Part 1: Setup the MATTR VII Verifier tenant

The MATTR VII tenant will be used to interact with your mobile application (generating a verification request) and the wallet application (presenting an mDoc for verification) as per OID4VP and ISO/IEC 18013-7 Annex B. To enable this, you must:

  1. Create a MATTR VII tenant: This is the tenant that will be used to perform the verification workflow.
  2. Create a verifier application configuration: Define what applications can create verification sessions with the MATTR VII tenant, and how to handle these requests.
  3. Create a supported wallet configuration: Define how to invoke specific wallet applications as part of a remote verification workflow.
  4. Configure a trusted issuer: The MATTR VII verifier tenant will only accept mDocs issued by these trusted issuers.

Create a MATTR VII tenant

If you already have a tenant you can skip this step.

  1. Log into the MATTR Portal.
  2. Select the Create/switch tenant button on the top-right side of the screen.
    The All tenants panel is displayed, listing any existing tenants.
  3. Select the Create new button.
    The New tenant form is displayed.
  4. Use the Region dropdown list to select the region your tenant will be hosted in.
  5. Use the Tenant subdomain text box to insert a subdomain for your tenant (e.g. remote-mobile-verification).
  6. Select the Create button to create the new tenant.
  7. Copy the displayed tenant information (audience, auth_url, tenant_url, client_id and client_secret) which is required for the next step.

Create a verifier application configuration

The iOS and Android Verifier SDKs must be tethered to a MATTR VII tenant. On initialization, the SDK registers your app instance with the tenant and obtains a license, so SDK Tethering must be configured before you initialize the SDK. For a full explanation of tethering and the capabilities it enables, see SDK Tethering.

For remote mobile (app-to-app) verification, the Verifier Application also defines the OID4VP redirect URI used to return the user to your app after the wallet presents the credential.

To enable SDK Tethering and configure the OID4VP redirect URI, create a Verifier Application on your MATTR VII tenant:

Make a request of the following structure to create an iOS Verifier Application configuration on your MATTR VII tenant:

Request
POST /v2/presentations/applications
Request body
{
    "name": "My iOS Mobile Verifier Application",
    "type": "ios",
    "bundleId": "com.yourname.mobileverifier",
    "teamId": "YOUR_APPLE_TEAM_ID",
    "appAttest": {
        "required": false,
        "environment": "development"
    },
    "openid4vpConfiguration": {
        "redirectUri": "com.yourname.mobileverifier://my/path"
    }
}
  • name : A unique name to identify this Verifier Application.
  • type : Must be ios.
  • bundleId : The Bundle ID of your iOS app (must match your Xcode project configuration).
  • teamId : Your Apple Developer Team ID.
  • appAttest : App Attest configuration for the iOS verifier application:
    • required : When true, the app instance must provide a valid App Attest attestation during registration and token renewal. When false, the app can fall back to assertion-only authentication. See Attestation vs Assertion for more details.
    • environment : The App Attest environment (development or production). Apple recommends using development for testing and production for distribution builds.
  • openid4vpConfiguration :
    • redirectUri : The URI the user is redirected to after presenting the credential from their wallet. The example uses the bundle ID as the scheme, but you can use any custom URL scheme. Only the scheme must match the custom URL scheme you register in the app; the path segment is illustrative.

A successful response returns a 201 status code with the created Verifier Application:

Response
{
    "id": "1ef1f867-20b4-48ea-aec1-bea7aff4964c", 
    "name": "My iOS Mobile Verifier Application",
    "type": "ios",
    // ... rest of application configuration
}
  • id: A unique identifier for the Verifier Application (generated by the tenant). You must use this value when initializing the SDK so that it can correctly identify and authenticate your application.

Make a request of the following structure to create an Android Verifier Application configuration on your MATTR VII tenant:

Request
POST /v2/presentations/applications
Request body
{
    "name": "My Android Mobile Verifier Application",
    "type": "android",
    "packageName": "com.example.mobileverifiertutorial",
    "packageSigningCertificateThumbprints": [
        "1232584B6F6A892D356899FB9576C5F226A179E6199F2B7A1D837B5C234C5A8E"
    ],
    "keyAttestation": {
        "required": false
    },
    "openid4vpConfiguration": {
        "redirectUri": "com.example.mobileverifiertutorial://oid4vp-callback"
    }
}
  • name: A unique name to identify this Verifier Application.
  • type: Must be android.
  • packageName: The package name of your Android application. Must match the package name you will define in your Android project later.
  • packageSigningCertificateThumbprints: SHA-256 hex-encoded fingerprints of the signing key certificates used to sign your APK or app bundle. This ensures the tenant only accepts requests from known and trusted applications. Refer to Android app signing for more information. We will update this value later in the tutorial after you generate the signing key and obtain its thumbprint.
  • keyAttestation: Key Attestation configuration for the Android verifier application:
    • required: When true, the app instance must provide a valid Key Attestation during registration and token renewal. When false, the app can register and renew tokens using just an authentication assertion. See Attestation vs Assertion for more details.
  • openid4vpConfiguration:
    • redirectUri: The URI the user is redirected to after presenting the credential from their wallet. Structure it as {your_app_packageName}://oid4vp-callback.

A successful response returns a 201 status code with the created Verifier Application:

Response
{
    "id": "a82bfa46-72a0-4cde-b6cb-2a0de7e2f3c4", 
    "name": "My Android Mobile Verifier Application",
    "type": "android",
    // ... rest of application configuration
}
  • id: A unique identifier for the Verifier Application (generated by the tenant). You must use this value when initializing the SDK so that it can correctly identify and authenticate your application.

A React Native app runs on both iOS and Android, and the MATTR VII tenant validates requests differently for each platform. Create a Verifier Application for each platform you intend to run on by following the iOS and Android tabs above. Each platform's redirectUri must match that platform's URL scheme, so configure one Verifier Application per platform and use the matching application id at runtime.

Create a supported wallet configuration

Verifier applications can define specific wallet applications to accept mDocs from as part of their verification workflows. The MATTR VII verifier tenant needs to be configured with a specific URI scheme that will be used to invoke these wallets.

  1. Log in to the MATTR Portal (if you haven't already).
  2. In the navigation panel on the left-hand side, expand the Credential Verification menu.
  3. Click on Supported wallets.
  4. Click on Create new.
  5. Enter a meaningful Name for the new supported wallet (e.g. "My Supported Wallet").
  6. Enter mdoc-openid4vp:// in the Authorization Endpoint field. This is the URI scheme that will be used to invoke the wallet application. More information on applying different URI schemes and the resulting user experience can be found in the workflow page.
  7. Click on Create.

The authorizationEndpoint configured in the example above (mdoc-openid4vp://) is the default OID4VP scheme. While this is technically redundant, we chose to include this step to explain how to configure this endpoint for wallet application using different schemes. More information on applying different URI schemes and the resulting user experience can be found in the workflow page.

Make the following request to your MATTR VII tenant to create a trusted wallet provider configuration:

Request
POST /v2/presentations/wallet-providers
Request body
{
  "name": "My Trusted Wallet Provider",
  "openid4vpConfiguration": {
    "authorizationEndpoint": "mdoc-openid4vp://"
  }
}
  • name : Unique name to identify this trusted wallet provider.
  • authorizationEndpoint : URI scheme that will be used to invoke the wallet application. More information on applying different URI schemes and the resulting user experience can be found in the workflow page.

The authorizationEndpoint configured in the example above (mdoc-openid4vp://) is the default OID4VP scheme. While this is technically redundant, we chose to include this step to explain how to configure this endpoint for wallet application using different schemes.

Response

Response body
{
  "id": "99890c34-e4b7-4a23-84d6-e5de57114c00", 
  "name": "My Trusted Wallet Provider",
  "openid4vpConfiguration": {
    "authorizationEndpoint": "mdoc-openid4vp://"
  }
}
  • id : You will use this value later to indicate this is the wallet the verifier application expects to receive mDocs from.

Configure a trusted issuer

  1. In the navigation panel on the left-hand side, expand the Credential Verification menu.
  2. Click on Trusted issuers.
  3. Click on Create new.
  4. Copy and paste the following certificate in the Certificate PEM file field:
-----BEGIN CERTIFICATE-----
MIICYzCCAgmgAwIBAgIKXhjLoCkLWBxREDAKBggqhkjOPQQDAjA4MQswCQYDVQQG
EwJBVTEpMCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0Ew
HhcNMjQwMTE4MjMxNDE4WhcNMzQwMTE1MjMxNDE4WjA4MQswCQYDVQQGEwJBVTEp
MCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0EwWTATBgcq
hkjOPQIBBggqhkjOPQMBBwNCAASBnqobOh8baMW7mpSZaQMawj6wgM5e5nPd6HXp
dB8eUVPlCMKribQ7XiiLU96rib/yQLH2k1CUeZmEjxoEi42xo4H6MIH3MBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRFZwEOI9yq
232NG+OzNQzFKa/LxDAuBgNVHRIEJzAlhiNodHRwczovL21vbnRjbGlmZi1kbXYu
bWF0dHJsYWJzLmNvbTCBgQYDVR0fBHoweDB2oHSgcoZwaHR0cHM6Ly9tb250Y2xp
ZmYtZG12LnZpaS5hdTAxLm1hdHRyLmdsb2JhbC92Mi9jcmVkZW50aWFscy9tb2Jp
bGUvaWFjYXMvMjk0YmExYmMtOTFhMS00MjJmLThhMTctY2IwODU0NWY0ODYwL2Ny
bDAKBggqhkjOPQQDAgNIADBFAiAlZYQP95lGzVJfCykhcpCzpQ2LWE/AbjTGkcGI
SNsu7gIhAJfP54a2hXz4YiQN4qJERlORjyL1Ru9M0/dtQppohFm6
-----END CERTIFICATE-----
  1. Click on Add.

You must configure trusted issuers on your MATTR VII verifier tenant, as presented mDocs will only be verified if they had been issued by a trusted issuer.

This is achieved by providing the PEM certificate of the IACA used by these issuers to sign mDocs. In production environments the issuer can provide it out of band or you can obtain it via their issuer's metadata.

Make the following request to your MATTR VII tenant to configure a truster issuer:

Request
POST /v2/credentials/mobile/trusted-issuers
Request body
{
  "certificatePem": "-----BEGIN CERTIFICATE-----\nMIICYzCCAgmgAwIBAgIKXhjLoCkLWBxREDAKBggqhkjOPQQDAjA4MQswCQYDVQQG\nEwJBVTEpMCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0Ew\nHhcNMjQwMTE4MjMxNDE4WhcNMzQwMTE1MjMxNDE4WjA4MQswCQYDVQQGEwJBVTEp\nMCcGA1UEAwwgbW9udGNsaWZmLWRtdi5tYXR0cmxhYnMuY29tIElBQ0EwWTATBgcq\nhkjOPQIBBggqhkjOPQMBBwNCAASBnqobOh8baMW7mpSZaQMawj6wgM5e5nPd6HXp\ndB8eUVPlCMKribQ7XiiLU96rib/yQLH2k1CUeZmEjxoEi42xo4H6MIH3MBIGA1Ud\nEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRFZwEOI9yq\n232NG+OzNQzFKa/LxDAuBgNVHRIEJzAlhiNodHRwczovL21vbnRjbGlmZi1kbXYu\nbWF0dHJsYWJzLmNvbTCBgQYDVR0fBHoweDB2oHSgcoZwaHR0cHM6Ly9tb250Y2xp\nZmYtZG12LnZpaS5hdTAxLm1hdHRyLmdsb2JhbC92Mi9jcmVkZW50aWFscy9tb2Jp\nbGUvaWFjYXMvMjk0YmExYmMtOTFhMS00MjJmLThhMTctY2IwODU0NWY0ODYwL2Ny\nbDAKBggqhkjOPQQDAgNIADBFAiAlZYQP95lGzVJfCykhcpCzpQ2LWE/AbjTGkcGI\nSNsu7gIhAJfP54a2hXz4YiQN4qJERlORjyL1Ru9M0/dtQppohFm6\n-----END CERTIFICATE-----"
}

If you intend to test this tutorial with a credential different than the one recommended in our Testing device prerequisite, replace the certificatePem value with your own issuer's IACA.

A successful 201 response indicates that this issuer's certificate was added to your MATTR VII tenant's trusted issuer's list. This means that mDocs that use this IACA as their root certificate can be trusted and verified.

Part 2: Build a mobile application with mDocs verification capabilities

Now that the MATTR VII verifier tenant is properly configured, you can proceed with the steps required to embed verification capabilities into your mobile verifier application:

  1. Setup your environment: Setup the required infrastructure for your mobile application.
  2. Initialize the SDK: So that the SDK functions are available in your mobile application.
  3. Request a credential from wallet application: Build the capability to request an mDoc for verification from a wallet application.
  4. Display verification results:

Step 1: Environment setup

Step 1: Create a new project

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

Step 2: Unzip the dependencies file

  1. Unzip the MobileCredentialVerifierSDK.xcframework.zip file.
  2. Drag the MobileCredentialVerifierSDK.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: 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 Mobile Verifier Tutorial.
  2. Set the Package name to com.example.mobileverifiertutorial (the value you used for the packageName when you created the verifier application on your MATTR VII tenant).
  3. Select API 24 as the Minimum SDK version.
  4. Select Kotlin DSL as the Build configuration language.

Project configuration

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

Step 2: Add required dependencies

  1. Select the Project view.

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

  3. Unzip the mobile-credential-verifier-*version*.zip file and copy the unzipped global folder into the new repo folder.

  4. Open the settings.gradle.kts file in the MobileVerifierTutorial 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:7.0.0")
    implementation("androidx.navigation:navigation-compose:2.9.0")
    • The verifier dependency version should match the version of the unzipped mobile-credential-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.
  6. Open the gradle/libs.versions.toml file and pin the following library versions:

    gradle/libs.versions.toml
    coreKtx = "1.18.0"
    lifecycleRuntimeKtx = "2.10.0"

    Newly scaffolded projects can pull in transitive AndroidX libraries (such as androidx.core:core-ktx and androidx.lifecycle:*) whose latest versions require a newer compileSdk and Android Gradle plugin than Android Studio scaffolded, which causes AAR metadata check failures on sync. Pinning these versions keeps them compatible with the scaffolded toolchain.

  7. Sync the project with Gradle files.

  8. 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 4: Update app signing certificate

  1. Retrieve your app's signing certificate thumbprint and convert it to the required format:

    • Open a terminal inside your project's root folder and run:

      Retrieve signing certificate
      ./gradlew signingReport

      If you see permission denied: ./gradlew, the Gradle wrapper has lost its executable bit (this happens when the project is downloaded as a zip rather than cloned). Make it executable and run the command again:

      Fix wrapper permissions
      chmod +x gradlew
    • Locate the SHA-256 value in the Variant: debug section, remove all colons (:), and convert all letters to lowercase. The result is the thumbprint you will send to your tenant in the next step:

      Example conversion
      echo '91:F7:CB:F9:D6:81:53:1B:C7:A5:8F:B8:33:CC:A1:4D:AB:ED:E5:09:C5' | tr -d ':' | tr 'A-F' 'a-f'

    This thumbprint is only valid for your debug builds. If you intend to publish your app, repeat this process with the release signing certificate. Refer to Android App Signing for more information.

  2. Update your verifier application with the formatted thumbprint by making a request of the following structure. Use the application id returned in Create a verifier application configuration as the {applicationId} path parameter, and set packageSigningCertificateThumbprints to the thumbprint you generated in the previous step:

    Request
    PUT /v2/presentations/applications/{applicationId}
    Request body
    {
        "name": "My Android Mobile Verifier Application",
        "type": "android",
        "packageName": "com.example.mobileverifiertutorial",
        "packageSigningCertificateThumbprints": [
            "91f7cbf9d681531bc7a58fb833cca14dabede509c5"
        ],
        "openid4vpConfiguration": {
            "redirectUri": "com.example.mobileverifiertutorial://oid4vp-callback"
        }
    }

    The PUT request replaces the entire application configuration, so include all the fields you set when you created the application in Create a verifier application configuration, not just the thumbprint.

    A successful response returns a 200 status code with the updated Verifier Application:

    Response
    {
        "id": "a82bfa46-72a0-4cde-b6cb-2a0de7e2f3c4",
        "name": "My Android Mobile Verifier Application",
        "type": "android",
        "packageSigningCertificateThumbprints": [
            "91f7cbf9d681531bc7a58fb833cca14dabede509c5"
        ],
        // ... rest of application configuration
    }

Step 1: Access the tutorial starter 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-remote-verification-tutorial/react-native-remote-verification-tutorial-starter/ directory.

You can find the completed tutorial code in the sample-apps/react-native-remote-verification-tutorial/react-native-remote-verification-tutorial-complete directory and use it as a reference as you work through this tutorial.

Step 2: Configure the app identifiers

  1. Open the app.config.ts file and update the bundleIdentifier value under the // Update the bundle identifier comment to the iOS bundle ID you used when you created the verifier application:

    app.config.ts
    bundleIdentifier: "global.mattr.learn.rnremoteverifiersampleapp",
  2. Update the package value under the // Update the package name comment to the Android package name you used when you created the verifier application:

    app.config.ts
    package: "global.mattr.learn.rnremoteverifiersampleapp",
  3. Add the custom URL scheme under the // Add the custom URL scheme used for the wallet redirect comment. This registers the scheme the wallet uses to redirect back to your app. Set it to your iOS bundle identifier:

    app.config.ts
    scheme: "global.mattr.learn.rnremoteverifiersampleapp",

Step 3: Configure the app plugins

Add the following code under the // Configure the app plugins comment to register the required plugin configurations:

app.config.ts
    "./withMobileCredentialAndroidVerifierSdk",
    "./withOpenid4VpCallbackActivity",
    [
      "expo-build-properties",
      {
        android: {
          minSdkVersion: 24,
          compileSdkVersion: 36,
          targetSdkVersion: 34,
          kotlinVersion: "2.0.21",
        },
      },
    ],

These plugin files have already been created in your project root directory:

  • withMobileCredentialAndroidVerifierSdk adds the Maven repository that hosts the native Android Verifier SDK.
  • withOpenid4VpCallbackActivity declares the SDK's OpenID4VP callback activity in AndroidManifest.xml, so the wallet can redirect back to your app on Android. The activity is bound to {your_packageName}://oid4vp-callback.

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-remote-verification-tutorial/react-native-remote-verification-tutorial-starter/
  2. Install the application dependencies:

    Install dependencies
    yarn install

Step 5: Generate the iOS and Android project files

Run the following command to generate the native 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 and run the following command for your target platform:

Run iOS application
yarn ios --device
Run Android application
yarn android --device

The app should launch with a single Request credentials button.

Step 7 (Android only): Update the app signing certificate

  1. From your project root, change into the generated android directory and retrieve your application's signing certificate:

    Retrieve signing certificate
    cd android && ./gradlew signingReport
  2. Locate the SHA-256 value in the Variant: debug section, remove all colons (:), and convert all letters to lowercase:

    Example conversion
    echo '91:F7:CB:F9:D6:81:53:1B:C7:A5:8F:B8:33:CC:A1:4D:AB:ED:E5:09:C5' | tr -d ':' | tr 'A-F' 'a-f'
  3. Update your Android verifier application with this thumbprint by making a PUT /v2/presentations/applications/{applicationId} request to your MATTR VII tenant, setting packageSigningCertificateThumbprints to the value you just generated (use the application id you created in Part 1).

    This thumbprint is only valid for your debug builds. If you intend to publish your app, repeat this process with the release signing certificate. Refer to Android App Signing for more information.

Step 2: Initialize the SDK

The first capability you will build into your app is to initialize the SDK so that the app can use its functions and classes. To achieve this, you need to import the MobilecredentialVerifierSDK framework and then initialize the MobileCredentialVerifier class.

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

    ContentView
    import SwiftUI
    // Step 2.3: Import MobileCredentialVerifierSDK
    
    struct ContentView: View {
        @State var viewModel: VerifierViewModel = VerifierViewModel()
    
        var body: some View {
            NavigationStack(path: $viewModel.navigationPath) {
                VStack {
                    Button("Request credentials") {
                        viewModel.requestCredentials()
                    }
                    .padding()
                }
                .navigationDestination(for: NavigationState.self) { destination in
                    switch destination {
                    case .viewResponse:
                        presentationResponseView
                    }
                }
            }
            // Step 4.2: Handle MATTR VII redirect
    
        }
    
        // MARK: Verification Views
    
        var presentationResponseView: some View {
            // Step 4.4: Create PresentationResponseView
            EmptyView()
        }
    }
    
    // MARK: VerifierViewModel
    
    @Observable
    final class VerifierViewModel {
        var navigationPath = NavigationPath()
        // Step 2.4: Setup platform configuration
    
        // Step 2.5: Add MobileCredentialVerifier var
    
        // Step 2.6: Initialize the SDK
    
        // Step 3.1: Create MobileCredentialRequest instance
    
        // Step 3.2: Create receivedDocuments variable
    
    }
    
    // MARK: Same Device Verification
    
    extension VerifierViewModel {
        func requestCredentials() {
        // Step 3.3: Request credentials
        }
    }
    
    // MARK: - Navigation
    enum NavigationState: Hashable {
        case viewResponse
    }

    This will serve as the basic structure for your application. You 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. Step 2.3: Import MobileCredentialVerifierSDK) to easily locate it in the code.

  2. Create a new file named Constants and paste the following code into it to define constants which are required to initialize the SDK:

    Constants
    import Foundation
    
    enum Constants {
        static let tenantHost = URL(string: "https://learn.vii.au01.mattr.global")!
        static let applicationID = "74f91a0f-5909-43b0-a431-6da2397d1f86"
    }
  3. Return to the ContentView file and add the following code after the Step 2.3: Import MobileCredentialVerifierSDK comment to import MobileCredentialVerifierSDK and gain access to the SDK capabilities:

    ContentView
    import MobileCredentialVerifierSDK
  4. Add the following code under the Step 2.4: Setup platform configuration comment to create a PlatformConfiguration instance:

    ContentView
        let platformConfiguration = PlatformConfiguration(
            tenantHost: Constants.tenantHost,
            applicationId: Constants.applicationID
        )

    This instance configures the MATTR VII tenant that the SDK will interact with (tenantHost) and the Verifier Application the SDK represents (applicationId), based on the constants defined in the Constants file. From v6.0.0, the applicationId is supplied here via PlatformConfiguration (it is no longer passed to requestMobileCredentials), and it also drives SDK Tethering.

  5. Add the following code after the Step 2.5: Add MobileCredentialVerifier var comment to create a variable that will hold the mobileCredentialVerifier instance when the SDK is initialized:

    ContentView
        var mobileCredentialVerifier: MobileCredentialVerifier
  6. Add the following code after the Step 2.6: Initialize the SDK comment to initialize the MobileCredentialVerifier instance with the parameters defined in the platformConfiguration instance:

    ContentView
        init() {
            mobileCredentialVerifier = MobileCredentialVerifier.shared
            Task {
                do {
                    try await mobileCredentialVerifier.initialize(platformConfiguration: platformConfiguration)
                } catch MobileCredentialVerifierError.failedToRegister {
                    // Registration with the MATTR VII tenant failed during SDK Tethering.
                    // Print the reason so the failure is visible: check connectivity and that
                    // tenantHost, applicationId, and the app's bundle identifier / team ID match
                    // the Verifier Application configuration.
                    print("SDK initialization failed to register:", MobileCredentialVerifierError.failedToRegister)
                } catch MobileCredentialVerifierError.invalidLicense {
                    // The SDK license is missing, invalid, or expired.
                    print("SDK initialization failed: invalid license")
                } catch {
                    print("SDK initialization failed:", error.localizedDescription)
                }
            }
        }

    The initialize method is asynchronous (called here from a Task) and drives SDK Tethering: on first launch it registers the app instance with your tenant and obtains a license. Network access is required the first time the SDK initializes and when the license is later renewed.

  7. Run the app to ensure it compiles successfully.

The first capability you will build into your app is to initialize the SDK so that the app can use its functions and classes. To achieve this, you need to import the MobilecredentialVerifierSDK framework and then initialize the MobileCredentialVerifier class.

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

    MainActivity
    package com.example.mobileverifiertutorial
    
    import android.app.Activity
    import android.os.Bundle
    import android.util.Log
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import androidx.compose.foundation.layout.*
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.items
    import androidx.compose.material3.Button
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.collectAsState
    import androidx.compose.runtime.getValue
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.tooling.preview.Preview
    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.lifecycleScope
    import androidx.lifecycle.viewModelScope
    import androidx.lifecycle.viewmodel.compose.viewModel
    import global.mattr.mobilecredential.verifier.deviceretrieval.devicerequest.DataElements
    import global.mattr.mobilecredential.verifier.deviceretrieval.devicerequest.NameSpaces
    import global.mattr.mobilecredential.verifier.dto.MobileCredentialPresentation
    import global.mattr.mobilecredential.verifier.dto.MobileCredentialRequest
    import global.mattr.mobilecredential.verifier.MobileCredentialVerifier
    import global.mattr.mobilecredential.verifier.OnlinePresentationSessionResult
    import global.mattr.mobilecredential.verifier.platformconfig.PlatformConfiguration
    import global.mattr.mobilecredential.verifier.exception.VerifierException.FailedToRegisterException
    import global.mattr.mobilecredential.verifier.exception.VerifierException.InvalidLicenseException
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.StateFlow
    import kotlinx.coroutines.launch
    import java.net.URL
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            enableEdgeToEdge()
            setContent {
                MobileVerifierTutorialTheme {
                    Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                        Content(
                            modifier = Modifier.padding(innerPadding)
                        )
                    }
                }
            }
    
            // Step 2.3: Setup platform configuration
    
            // Step 2.4: Initialize the SDK
    
        }
    }
    
    @Composable
    fun Content(modifier: Modifier = Modifier) {
        val activity = (LocalContext.current) as Activity
        val viewModel: VerifierViewModel = viewModel()
    
        val documents by viewModel.receivedDocuments.collectAsState()
    
        Column(
            modifier = modifier
                            .fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(onClick = {
                viewModel.requestCredentials(activity)
            }) {
                Text("Request credentials")
            }
            LazyColumn(modifier = Modifier.fillMaxWidth()) {
                items(documents) { document ->
                    // Step 4.5: Display received documents
    
                }
            }
        }
    }
    
    @Preview(showBackground = true)
    @Composable
    fun ContentPreview() {
        MobileVerifierTutorialTheme {
            Content()
        }
    }
    
    class VerifierViewModel : ViewModel() {
        private val _receivedDocuments =
            MutableStateFlow<List<MobileCredentialPresentation>>(emptyList())
        val receivedDocuments: StateFlow<List<MobileCredentialPresentation>> = _receivedDocuments
    
        fun requestCredentials(activity: Activity) {
            // Step 3.1: Create MobileCredentialRequest instance
    
            viewModelScope.launch {
                _receivedDocuments.value = emptyList()
                try {
                    // Step 3.2: Request credentials
    
                    // Step 4.2: Handle response
    
                } catch (e: Exception) {
                    e.printStackTrace()
                }
            }
        }
    }

    This will serve as the basic structure for your application. You 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 the Android Studio search field (e.g. Step 2.3: Setup platform configuration) to easily locate it in the code.

  2. Create a new file named Constants and paste the following code, replacing the values as indicated, to define constants which are required to initialize the SDK:

    Constants
    package com.example.mobileverifiertutorial
    
    object Constants {
        const val TENANT_HOST = "https://learn.vii.au01.mattr.global"
        const val APPLICATION_ID = "74f91a0f-5909-43b0-a431-6da2397d1f86"
    }
    • TENANT_HOST : Replace with the URL of your MATTR VII tenant. This indicates the tenant that the SDK will interact with.
    • APPLICATION_ID : Replace with the id returned when you created the MATTR VII verifier application. This indicates the verifier application that the SDK will represent.
  3. Add the following code under the Step 2.3: Setup platform configuration comment to create a PlatformConfiguration instance:

    MainActivity
    val platformConfiguration = PlatformConfiguration(
        tenantHost = URL(Constants.TENANT_HOST),
        applicationId = Constants.APPLICATION_ID
    )

    This instance configures the MATTR VII tenant that the SDK will interact with (tenantHost) and the Verifier Application the SDK represents (applicationId), based on the constants defined in the Constants file.

  4. Add the following code after the Step 2.4: Initialize the SDK comment to initialize the MobileCredentialVerifier instance with the parameters defined in the platformConfiguration instance:

    MainActivity
    lifecycleScope.launch {
        try {
            MobileCredentialVerifier.initialize(
                context = this@MainActivity, platformConfiguration = platformConfiguration
            )
        } catch (e: FailedToRegisterException) {
            // Registration with the MATTR VII tenant failed during SDK Tethering.
            // Log the reason so the failure is visible: check connectivity and that
            // tenantHost, applicationId, and the app's package name / signing certificate
            // thumbprint match the Verifier Application configuration.
            Log.e("VerifierTutorial", "SDK initialization failed to register", e)
        } catch (e: InvalidLicenseException) {
            // The SDK license is missing, invalid, or expired.
            Log.e("VerifierTutorial", "SDK initialization failed: invalid license", e)
        }
    }
  5. Run the app to ensure it compiles successfully.

The first capability you will build into your app is to initialize the SDK so that the app can use its functions and classes. To achieve this, you import the SDK and call initialize with your tenant host configuration.

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

    App.tsx
    import {
      type MobileCredentialResponse,
      handleDeepLink,
      initialize,
      requestMobileCredentials,
    } from "@mattrglobal/mobile-credential-verifier-react-native";
    // import { VerificationResultsModal } from "./VerificationResultsModal";
    
    import * as Crypto from "expo-crypto";
    import { StatusBar } from "expo-status-bar";
    import { useEffect, useState } from "react";
    import { ActivityIndicator, Alert, Linking, Platform, SafeAreaView, Text, TouchableOpacity, View } from "react-native";
    
    import { Constants } from "./Constants";
    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 [verificationResults, setVerificationResults] = useState<MobileCredentialResponse | null>(null);
      const [showVerificationResults, setShowVerificationResults] = useState(false);
    
      // Step 2: Initialize the SDK
    
      // Step 4.1: Handle the wallet redirect (iOS)
    
      // Step 3: Request credentials
    
      return (
        <SafeAreaView style={styles.container}>
          <StatusBar style="auto" />
          <View style={styles.header}>
            <Text style={styles.title}>mDocs Remote 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}>
                {/* Step 3: Add the Request credentials button */}
              </View>
              {!isSDKInitialized && <Text style={styles.errorText}>SDK not initialized. Please restart the app.</Text>}
            </View>
          )}
    
          {/* Step 4.3: Display the verification results */}
        </SafeAreaView>
      );
    }

    This will serve as the basic structure for your application. You will add code to specific locations to achieve the different functionalities. These locations are indicated by comments that reference the step.

    We recommend using your editor's search functionality to locate comments like // Step 2: Initialize the SDK when adding new code.

  2. Create a new file named Constants.ts and paste the following code, replacing the values as indicated, to define the constants required to initialize the SDK and request credentials:

    Constants.ts
    import { Platform } from "react-native";
    
    /**
     * Configuration values used to initialize the SDK and request credentials.
     *
     * Replace these placeholders with your own values before running the app:
     * - `TENANT_HOST`: the URL of your MATTR VII tenant, available in the MATTR Portal under
     *   Platform Management > Tenant.
     * - `IOS_APPLICATION_ID` / `ANDROID_APPLICATION_ID`: the `id` returned when you created the verifier
     *   application configuration on your MATTR VII tenant (see Part 1).
     *   iOS and Android each use their own verifier application, because the redirect URI registered on
     *   the application must match that platform's URL scheme (see `app.config.ts`). Configure one
     *   application ID per platform.
     */
    const IOS_APPLICATION_ID = "your-ios-application-id";
    const ANDROID_APPLICATION_ID = "your-android-application-id";
    
    export const Constants = {
      TENANT_HOST: "https://your-tenant.vii.mattr.global",
      // Resolves to the verifier application ID for the current platform.
      APPLICATION_ID: Platform.OS === "ios" ? IOS_APPLICATION_ID : ANDROID_APPLICATION_ID,
    };
    • TENANT_HOST : Replace with the URL of your MATTR VII tenant. This indicates the tenant that the SDK will interact with.
    • IOS_APPLICATION_ID and ANDROID_APPLICATION_ID : Create one verifier application per platform you support, each with a redirect URI that matches that platform's URL scheme. You configure these schemes in app.config.ts in Step 1: Environment setup: iOS uses {scheme}://my/path and Android uses {package}://oid4vp-callback. Because the redirect URI on each verifier application must match the scheme its platform uses, each platform needs its own application. Paste the iOS application id into IOS_APPLICATION_ID and the Android application id into ANDROID_APPLICATION_ID. Each id is the value returned when you created that platform's verifier application in Part 1.
    • APPLICATION_ID : Resolves automatically to the verifier application id for the current platform at runtime. The SDK reads this value when requesting credentials, so you do not need to change anywhere it is consumed.
  3. Return to the App.tsx file and add the following code under the // Step 2: Initialize the SDK comment to initialize the SDK with your tenant host:

    App.tsx
      useEffect(() => {
        const initializeSDK = async () => {
          try {
            setLoadingMessage("Initializing SDK...");
            const result = await initialize({
              platformConfiguration: { tenantHost: Constants.TENANT_HOST },
            });
            if (result.isErr()) {
              console.error("Failed to initialize SDK:", result.error);
              Alert.alert("Error", "Failed to initialize the verifier SDK");
              return;
            }
            setIsSDKInitialized(true);
          } catch (error) {
            console.error("Failed to initialize SDK:", error);
            Alert.alert("Error", "Failed to initialize the verifier SDK");
          } finally {
            setLoadingMessage(false);
          }
        };
    
        initializeSDK();
      }, []);

    Unlike the in-person (proximity) flow, the remote flow requires a tenantHost: the SDK starts presentation sessions with this MATTR VII tenant, which performs the verification server-side and returns the results. The SDK uses a Result type for expected errors, so you check result.isErr() rather than relying on a thrown exception.

  4. Run the app to ensure it compiles successfully.

Step 3: Request a credential from wallet application

Once the SDK is initialized, you can start building the capabilities to request an mDoc for verification from a wallet application. This is done by:

  1. Creating a request object that defines what information is required for verification.
  2. Sending this request to the wallet application installed on the device.
  3. Redirecting the user to the wallet application to present the requested mDoc.
  1. Open the ContentView file and add the following code under the Step 3.1: Create MobileCredentialRequest instance comment to create a new MobileCredentialRequest instance:

    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 defines what information is required for verification:

    • 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). Declarative only and not currently enforced by the SDK. 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.
  2. Add the following code under the Step 3.2: Create receivedDocuments variable comment to create a new receivedDocuments variable that will hold the response received from the wallet application:

    ContentView
        var receivedDocuments: [MobileCredentialPresentation] = []
  3. Add the following code under the Step 3.3: Request credentials comment to call the SDK's requestMobileCredentials method:

    ContentView
            Task { @MainActor in
                // Clean the response before fetching a new one
                receivedDocuments = []
                do {
                    let onlinePresentationResult = try await mobileCredentialVerifier.requestMobileCredentials(request: [mobileCredentialRequest])
                    // From v6.0.0, OnlinePresentationSessionResult is a @frozen enum with
                    // success and failure cases.
                    switch onlinePresentationResult {
                    case .success(_, _, let mobileCredentialResponse):
                        receivedDocuments = mobileCredentialResponse?.credentials ?? []
                    case .failure(_, _, let error):
                        print("No response received: \(error.message)")
                        return
                    }
                } catch {
                    print(error.localizedDescription)
                }
            }

    The following parameter is passed to the requestMobileCredentials method:

    • request : Defines what information to request. This example is passing the mobileCredentialRequest instance you created in the previous step.

    From v6.0.0, the applicationId is no longer passed here — it is supplied via PlatformConfiguration when you initialize the SDK. The challenge parameter is now optional: when omitted, the SDK generates a cryptographically secure challenge for you. You can still pass your own challenge (a unique, unpredictable value per session) to mitigate replay attacks if you prefer to manage it yourself.

  4. Run the app and press Request credentials button.

    You will be redirected to a compliant wallet application, where you will see the verification request details and choose what mDoc to present for verification.

Once you send the response from the wallet nothing will happen, which is expected at this stage. In the next step you will build the capability to redirect the user back to the verifier application and handle the response from the wallet.

  1. Open the MainActivity file and add the following code under the Step 3.1: Create MobileCredentialRequest instance comment to create a new MobileCredentialRequest instance:

    MainActivity
     val mobileCredentialRequest = MobileCredentialRequest(
         docType = "org.iso.18013.5.1.mDL", namespaces = NameSpaces(
             mapOf(
                 "org.iso.18013.5.1" to DataElements(
                     mapOf(
                         "family_name" to false, "given_name" to false, "birth_date" to false
                     )
                 )
             )
         )
     )

    This object defines what information is required for verification:

    • 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). Declarative only and not currently enforced by the SDK.

    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.

  2. Add the following code under the Step 3.2: Request credentials comment to call the SDK's requestMobileCredentials method:

    MainActivity
    val onlinePresentationResult = MobileCredentialVerifier.requestMobileCredentials(
        activity = activity,
        request = listOf(mobileCredentialRequest)
    )

    The following parameters are passed to the requestMobileCredentials method:

    • activity : Defines the current activity context.
    • request : Defines what information to request. This example is passing the mobileCredentialRequest instance you created in the previous step.

    From v7.0.0, requestMobileCredentials no longer takes an applicationId argument — the SDK uses the applicationId you supplied via PlatformConfiguration when you initialized the SDK.

  3. Run the app and press Request credentials button.

    You will be redirected to a compliant wallet application, where you will see the verification request details and choose what mDoc to present for verification.

Once you send the response from the wallet nothing will happen, which is expected at this stage. In the next step you will build the capability to redirect the user back to the verifier application and handle the response from the wallet.

  1. Open the App.tsx file and add the following code under the // Step 3: Request credentials comment to create a function that builds a request and starts a remote presentation session:

    App.tsx
      const requestCredentials = async () => {
        try {
          setVerificationResults(null);
          setLoadingMessage("Requesting credentials...");
    
          // Define what information to request:
          // - docType: the requested credential type (org.iso.18013.5.1.mDL)
          // - namespaces: the requested namespace (org.iso.18013.5.1) and claims
          // - Each claim value (false) indicates the verifier does NOT intend to retain the data.
          const mobileCredentialRequest = {
            docType: "org.iso.18013.5.1.mDL",
            namespaces: {
              "org.iso.18013.5.1": {
                family_name: false,
                given_name: false,
                birth_date: false,
              },
            },
          };
    
          const result = await requestMobileCredentials({
            request: [mobileCredentialRequest],
            applicationId: Constants.APPLICATION_ID,
            challenge: Crypto.randomUUID(),
          });
    
          if (result.isErr()) {
            throw new Error(`Failed to request credentials: ${result.error.message}`);
          }
    
          const session = result.value;
          if (!session.isSuccess) {
            throw new Error(`Verification session failed: ${session.error.message}`);
          }
    
          const response = session.mobileCredentialResponse;
          if (!response) {
            throw new Error("No verification results were returned by the tenant.");
          }
    
          setVerificationResults(response);
          setShowVerificationResults(true);
        } catch (error) {
          console.error("Error requesting credentials:", error);
          Alert.alert("Error", error instanceof Error ? error.message : String(error));
        } finally {
          setLoadingMessage(false);
        }
      };

    The following parameters are passed to the requestMobileCredentials method:

    • request : Defines what information to request. This example passes the mobileCredentialRequest object you defined above. For the verification to be successful, the presented credential must include the referenced claims against the specific namespace defined in the request.
    • applicationId : Identifier of the verifier application that will be used to verify the request. In this example it is retrieved from the Constants file.
    • challenge : A unique, unpredictable value generated for each verification session to mitigate replay attacks. This example uses Crypto.randomUUID() from expo-crypto. Always generate a new challenge for every request.
  2. Add the following code under the {/* Step 3: Add the Request credentials button */} comment to add a button that invokes the requestCredentials function:

    App.tsx
                <TouchableOpacity
                  style={[styles.button, !isSDKInitialized && styles.buttonDisabled]}
                  onPress={requestCredentials}
                  disabled={!isSDKInitialized}
                >
                  <Text style={styles.buttonText}>Request credentials</Text>
                </TouchableOpacity>
  3. Run the app and press the Request credentials button.

    You will be redirected to a compliant wallet application, where you will see the verification request details and choose what mDoc to present for verification.

Once you send the response from the wallet nothing will happen, which is expected at this stage. In the next step you will build the capability to handle the response from the wallet and display the verification results.

Step 4: Display verification results

Once the user provides their consent to share the requested information, the wallet application will send the response back to the MATTR VII tenant, which will then return the verification results to your verifier application and redirect the user back to the configured redirect URI. In this part of the tutorial you will build the capability to handle this redirect and display the verification results in your application.

To enable the redirect back to your verifier application you must register a redirection link. This could be either a Universal link or a custom URL scheme. In this tutorial you will use a custom URL scheme.

  1. Register a custom URL scheme in your verifier application:

    • Open the project view and select your application target.
    • Select the Info tab.
    • Scroll down and expand the URL Types area.
    • Select the plus button.
    • Insert your app bundle identifier (as set when you configured the MATTR VII verifier application) in both the Identifier and URL Schemes fields.

Custom URL registration

  1. In your ContentView file, add the following code under the Step 4.2: Handle MATTR VII redirect comment to handle the redirect from the wallet application:

    ContentView
            .onOpenURL { url in
                // Navigate to response screen
                viewModel.navigationPath.append(NavigationState.viewResponse)
                viewModel.mobileCredentialVerifier.handleDeepLink(url)
            }

    This will pass the redirect URL to the SDK's handleDeepLink method, which will process the response from the wallet application and update the receivedDocuments variable with the verification results.

  2. Create a new file named DocumentView and add the following code to display the retrieved 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
    
        @Observable
        class DocumentViewModel {
            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.failureType?.rawValue
    
                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 that are displayed in the DocumentView.
    • Extension of MobileCredentialElementValue which converts the values of received claims into a human-readable format.
  3. Return to the ContentView file and replace the EmptyView() under the Step 4.4: 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)
                    }
                }
            }
        }

To enable the redirect back to your verifier application you must register a redirection link. This could be either App Links or a Custom deep links. In this tutorial you will use a custom deep link.

  1. In your app's AndroidManifest.xml file, add the following activity declaration to register the callback url:

    AndroidManifest.xml
    <activity
        android:name="global.mattr.mobilecredential.verifier.a2apresentation.callback.Openid4VpCallbackActivity"
        android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <!-- Match the openid4vpConfiguration you configured in the MATTR VII Verifier application -->
        <data
            android:scheme="com.example.mobileverifiertutorial"
            android:host="oid4vp-callback" />
      </intent-filter>
    </activity>
  2. Add the following code under the Step 4.2: Handle response to navigate the user to the response screen, where they can see the retrieved credentials, if the retrieval was successful:

    MainActivity.kt
    _receivedDocuments.value = when (onlinePresentationResult) {
        is OnlinePresentationSessionResult.Success ->
            onlinePresentationResult.mobileCredentialResponse?.credentials ?: emptyList()
        is OnlinePresentationSessionResult.Failure -> {
            // onlinePresentationResult.error is available here
            emptyList()
        }
    }
  3. Create a new file named DocumentView.kt that will be used to display the response to the verifier application user.

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

    DocumentView.kt
    package com.example.mobileverifiertutorial
    
    import androidx.compose.foundation.layout.*
    import androidx.compose.material3.Card
    import androidx.compose.material3.CardDefaults
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.graphics.Color
    import androidx.compose.ui.text.font.FontWeight
    import androidx.compose.ui.unit.dp
    import global.mattr.mobilecredential.verifier.dto.MobileCredentialPresentation
    
    @Composable
    fun DocumentView(document: MobileCredentialPresentation, modifier: Modifier = Modifier) {
        val verified: Boolean = document.verificationResult.verified
        val statusText: String = if (verified) "Verified" else "Invalid"
        val statusColor: Color = if (verified) Color.Green else Color.Red
        val flatClaims: List<String> = document.claims?.flatMap { (_, claimsMap) ->
            claimsMap.map { (claim, value) -> "$claim: ${value.value}" }
        } ?: emptyList()
    
        Card(
            modifier = modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(containerColor = Color.White),
            elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(
                    text = document.docType,
                    color = Color.Black,
                    style = MaterialTheme.typography.titleLarge,
                    fontWeight = FontWeight.Bold,
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text = statusText,
                    color = statusColor,
                    style = MaterialTheme.typography.titleMedium,
                    fontWeight = FontWeight.SemiBold
                )
                Spacer(modifier = Modifier.height(12.dp))
                if (flatClaims.isEmpty()) {
                    Text(
                        text = "No claims",
                        color = Color.Black,
                        style = MaterialTheme.typography.labelMedium,
                    )
                } else {
                    flatClaims.forEach { line ->
                        Text(
                            text = line,
                            color = Color.Black,
                            style = MaterialTheme.typography.labelMedium
                        )
                    }
                }
            }
        }
    }
  5. Back in the MainActivity.kt file, add the following code under the Step 4.5: Display received documents comment:

    MainActivity.kt
    DocumentView(document)

After the wallet presents the credential, the user is redirected back to your app and the SDK makes the verification results available. Handling this redirect differs by platform:

  • iOS: The wallet redirects back to your app via the custom URL scheme you registered in app.config.ts. You must forward the redirect URL to the SDK so it can complete the pending requestMobileCredentials call.
  • Android: The withOpenid4VpCallbackActivity plugin you configured in Step 1: Environment setup declared the SDK's Openid4VpCallbackActivity in AndroidManifest.xml. The SDK handles the redirect automatically, so requestMobileCredentials resolves directly with the result and no additional code is required.
  1. In your App.tsx file, add the following code under the // Step 4.1: Handle the wallet redirect (iOS) comment to forward the redirect URL to the SDK on iOS:

    App.tsx
      useEffect(() => {
        if (Platform.OS !== "ios") {
          return;
        }
        const subscription = Linking.addEventListener("url", ({ url }) => {
          handleDeepLink({ url });
        });
        return () => subscription.remove();
      }, []);

    This passes the redirect URL to the SDK's handleDeepLink method, which processes the response from the wallet and resolves the pending requestMobileCredentials call so it returns the verification results.

  2. Create a new file named VerificationResultsModal.tsx and paste the following code to display the retrieved verification results:

    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;
    
      // mDoc claims can have various types (string, number, date, array, object, etc.).
      // Arrays and objects are serialized to JSON; all other types use String conversion.
      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>
                  {/* Overall verification status for the first credential, as determined by the tenant. */}
                  <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>
    
                  {/* Claims organized by namespace. */}
                  {verificationResults.credentials.map((credential, credIndex) => (
                    <View key={`credential-${credIndex}`}>
                      {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>
                        ))}
    
                      {/* Verification failure reason, if the mDoc did not pass verification. */}
                      {!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: claims that were requested but not provided. */}
                      {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>
      );
    }

    This component reads each MobileCredentialPresentation from the MobileCredentialResponse and renders the docType, the overall verification result, the received claims grouped by namespace, and any claim errors.

  3. Back in App.tsx, uncomment the VerificationResultsModal import at the top of the file:

    App.tsx
    import { VerificationResultsModal } from "./VerificationResultsModal";
  4. Add the following code under the {/* Step 4.3: Display the verification results */} comment to render the modal when results are available:

    App.tsx
          <VerificationResultsModal
            visible={showVerificationResults}
            onClose={() => setShowVerificationResults(false)}
            verificationResults={verificationResults}
          />

Test the end-to-end workflow

  1. Run the app.
  2. Select the Request credentials button.
    You should be redirected to a compliant wallet application, where you will see the verification request details and choose what mDoc to present for verification.
  3. Use the wallet application to present the requested mDoc.
    You will be redirected back to the verifier application where you will see the verification results.

You should see a result similar to the following:

The React Native app follows the same end-to-end flow as the iOS and Android apps shown above: the verifier app starts a presentation session, redirects to a compliant wallet, and displays the verification results returned by the MATTR VII tenant once the user consents to share the requested information.

  1. The verifier app starts a presentation session and gets redirected.
  2. The user is redirected to a compliant wallet application.
  3. The user provides their consent to share the requested information.
  4. The wallet application sends the response back to the MATTR VII tenant.
  5. The MATTR VII tenant redirects the user back to the verifier app with the verification results.
  6. The verifier app fetches the result and presents the result to user.

Congratulations! Your verifier application can now verify mDocs presented from a compliant wallet installed on the same mobile device.

Summary

You have just used the mDocs Mobile Verifier SDKs to build an application that can verify an mDoc presented from a compliant wallet on the same device using a remote presentation workflow as per OID4VP and ISO/IEC 18013-7 Annex B.

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

  • Initialize the SDK, so that your application can use its functions and classes.
  • Request an mDoc for verification from a compliant wallet application.
  • Display verification results in your verifier application.

What's next?

  • You can check out the SDKs reference documentation to learn more about available functions and classes:

How would you rate this page?

Last updated on

On this page