Learn how to build an application that can present an mDoc remotely
Introduction
In this tutorial you will use the mDocs Holder SDKs to build an application that can present a claimed mDoc to a verifier remotely via a remote presentation workflow as per ISO/IEC 18013-7:2025 and OID4VP.
This app will support both same-device and cross-device workflows to accommodate flexible user journeys.
Same-device workflow
- The user interacts with a website on their mobile device browser.
- The user is asked to present information as part of the interaction.
- The user is redirected to the application you will build in this tutorial.
- The application authenticates the user.
- The user is informed of what information they are about to share and provide their consent.
- The user is redirected back to the browser where verification results are displayed, enabling them to continue with the interaction.
The result will look something like this:
Cross-device workflow
- The user interacts with a website on their desktop browser.
- The user is asked to present information as part of the interaction.
- The user scans a QR code using a mobile device where the tutorial application is installed.
- The tutorial application is launched on the mobile device.
- The tutorial application authenticates the user.
- The user is informed of what information they are about to share and provide their consent.
- Verification results are displayed in the user's desktop browser, enabling them to continue with the interaction.
The result will look something like this:
Prerequisites
Before you get started, let's make sure you have everything you need.
Prior knowledge
-
The verification workflow described in this tutorial is based on ISO/IEC 18013-7:2025 and OID4VP. If you are unfamiliar with these technical specifications, refer to the following resources for more information:
- What are mDocs?
- What is credential verification?
- Breakdown of the remote presentation workflow.
-
We assume you have experience developing applications in the relevant programming languages and frameworks (Swift for iOS, Kotlin for Android and TypeScript for React Native).
If you need to get a holding solution up and running quickly with minimal development resources and in-house domain expertise, talk to us about our white-label MATTR GO Hold app 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:
(
MobileCredentialHolderSDK-*version*.xcframework.zip
). - Sample Wallet app: You can use this app for reference as you work through this tutorial.
This tutorial is only meant to be used with the most recent version of the iOS mDocs Holder SDK.
As part of your onboarding process you should have been provided with access to the following assets:
- ZIP file which includes the required library: (
holder-*version*.zip
). - Sample Wallet app: You can use this app for reference as you work through this tutorial.
This tutorial is only meant to be used with the most recent version of the Android mDocs Holder SDK.
You will need access to the SDK and additional MATTR dependencies to complete this tutorial. Contact us if you are interested in trialing the SDK.
This tutorial is intended for use with the latest version of MATTR's React Native mDocs Holder 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.
- Code editor (such as VS Code).
- Android Studio.
- Xcode.
- yarn (v1.22.22 was used during development).
- Java v17.
This tutorial uses Expo Go, leveraging Development Builds.
Prerequisite tutorial
- You must complete the Claim a credential tutorial and claim the mDoc provided in the tutorial.
- This application is used as the base for the current tutorial.
Testing devices
- Supported iOS device to run the built application on, setup with:
- Biometric authentication (Face ID, Touch ID).
- Available internet connection.
- Supported Android device to run the built application on, setup with:
- Biometric authentication (Face recognition, fingerprint recognition).
- Available internet connection.
- Debugging enabled.
- Supported iOS and/or Android device to run the built application on, setup with:
- Available internet connection.
- iOS:
- Biometric authentication (Face ID, Touch ID).
- Android:
- Biometric authentication (Face recognition, fingerprint recognition).
- Debugging enabled.
Got everything? Let's get going!
Tutorial steps
To enable a user to present a stored mDoc to a verifier via an online presentation workflow, you will build the following capabilities into your application:
- Register the verifier's Authorization endpoint.
- Create an online presentation session.
- Handle a presentation request.
- Send a presentation response.
Step 1: Register the verifier's Authorization endpoint
The Authorization endpoint is a URI associated with an application identifier in the MATTR VII tenant configuration. It is used to invoke an application that will handle the presentation request. The application then uses the URI to retrieve a request object, which details what information is required for verification.
Online verifiers are recommended to generate this URI as a Universal link for iOS and App Link for Android, as this enables them to explicitly define and validate applications that can respond to their verification requests.
However, for simplicity reasons in this tutorial our verifier is using a
custom URI scheme
for iOS and a deep link for Android,
both matching the default scheme defined by the OID4VP specification (mdoc-openid4vp
). This means
that you need to configure the application to be able to handle this custom URI scheme.
-
Open the Xcode project with the application built in the Claim a credential tutorial.
-
Register
mdoc-openid4vp
as a recognized URL scheme:- 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
mdoc-openid4vp
in both the Identifier and URL Schemes fields.
-
Run the app and then close it (this updates the app on your testing device) and perform the following instructions:
- Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
- Select any of the mDL options from the request templates list.
- Select Request credentials.
- Open the camera on your testing mobile device and scan the QR code.
- Confirm opening the QR code with your tutorial application.
- The tutorial application should be launched on your testing mobile device.
-
Open the Android Studio project with the application built in the Claim a credential tutorial.
-
Open your
AndroidManifest.xml
. -
Add the following intent filter to your
MainActivity
:AndroidManifest.xml <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="mdoc-openid4vp" /> </intent-filter>
-
Run the app and then close it (this updates the app on your testing device) and perform the following instructions:
- Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
- Select any of the mDL options from the request templates list.
- Select Request credentials.
- Open the camera on your testing mobile device and scan the QR code.
- Confirm opening the QR code with your tutorial application.
- The tutorial application should be launched on your testing mobile device.
-
Open the project with the application built in the Claim a credential tutorial.
-
Open the
app.config.ts
file and replace thescheme
property assignment under the// Online presentation - Step 1.1: Update application custom scheme
comment with the following:app.config.ts scheme: "mdoc-openid4vp",
-
Delete the
ios
andandroid
directories in the project's root. This is required to ensure the scheme changes are applied correctly.
Step 2: Create an online presentation session
Now that the application can handle an OID4VP custom URI scheme, the next step is to build the capability to use the request URI to retrieve the request object. This object details:
- What credentials are required.
- What specific claims are required from these credentials.
- What MATTR VII tenant to interact with.
-
In your project's
ContentView
file, add the following code under the// Online Presentation - Step 2.1: Create a variable to hold the online presentation session object
comment to create a variable that will hold the online presentation session:ContentView @Published var onlinePresentationSession: OnlinePresentationSession?
The following step is also included in the Proximity presentation tutorial. If you had already completed this tutorial you may skip to step 3.
-
Add the following code under the
// Proximity and Online Presentation: Create variables for credential presentations
comment to create the following variables:ContentView @Published var matchedCredentials: [MobileCredential] = [] @Published var matchedMetadata: [MobileCredentialMetadata] = [] @Published var credentialRequest: [MobileCredentialRequest] = []
matchedCredentials
: Holds stored credentials that match the credential request.matchedMetadata
: Holds metadata of credentials that match the credential request.credentialRequest
: Holds the credentials that were requested for verification.
-
Replace the
print
statement under the// Online Presentation - Step 2.3: Create online presentation session
comment with the following code to create a function that calls the SDK'screateOnlinePresentationSession
method with theauthorizationRequestURI
parameter (the request URI retrieved from the link/QR code):ContentView Task { do { onlinePresentationSession = try await mobileCredentialHolder.createOnlinePresentationSession(authorizationRequestUri: authorizationRequestURI, requireTrustedVerifier: false) matchedMetadata = onlinePresentationSession?.matchedCredentials? .flatMap { $0.matchedMobileCredentials } .compactMap { $0 } ?? [] credentialRequest = onlinePresentationSession?.matchedCredentials? .compactMap { $0.request } .compactMap { $0 } ?? [] } catch { print(error.localizedDescription) } }
This function:
- Creates an
OnlinePresentationSession
instance and assigns it to theonlinePresentationSession
variable. - Stores matched
MobileCredentialMetadata
in thematchedMetadata
variable and theMobileCredentialRequest
in thecredentialRequest
variable in ourViewModel
to enable displaying these values to the user.
- Creates an
We chose to set requireTrustedVerifier
parameter to false
because we want
the SDK to trust all verifiers by default. If you require to limit the
verifiers a user can interact with, you may want to manually add trusted
verifier certificates and set the parameter to true
. You can learn more
about certificate management in our SDK
docs.
-
Add the following code under the
// Online Presentation - Step 2.4: Create session from request URI
comment to add an onOpenURL modifier that will call thecreateOnlinePresentationSession
function when the application is launched following selecting a link (same-device flow) or scanning a QR code (cross-device flow) that includes a registered URI:ContentView .onOpenURL { url in Task { await viewModel.createOnlinePresentationSession(authorizationRequestURI: url.absoluteString) } // Navigate to online presentation view viewModel.navigationPath.append(NavigationState.onlinePresentation) }
Now, once a user opens an online presentation link on their device, an online presentation session will be created the user will be navigated to a new view, which you will implement in the next step.
-
Create a new file named
OnlinePresentationScreen.kt
and add the following code to the file:OnlinePresentationScreen.kt import android.app.Activity import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import global.mattr.mobilecredential.common.deviceretrieval.devicerequest.DataElements import global.mattr.mobilecredential.common.deviceretrieval.deviceresponse.NameSpace import global.mattr.mobilecredential.common.dto.MobileCredential import global.mattr.mobilecredential.common.dto.MobileCredentialMetaData import global.mattr.mobilecredential.holder.MobileCredentialHolder import global.mattr.mobilecredential.holder.onlinepresentation.OnlinePresentationSession import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable fun OnlinePresentationScreen(activity: Activity, requestUri: String) { var session: OnlinePresentationSession? by remember { mutableStateOf(null) } // Step 2.1: Create an online presentation session val (requested, matched) = session?.matchedCredentials?.entries?.firstOrNull() ?: return var matchedCredentials by remember { mutableStateOf(matched) } var selectedCredentialId by remember { mutableStateOf(matchedCredentials.first().id) } val coroutineScope = rememberCoroutineScope() Column(Modifier.verticalScroll(rememberScrollState())) { Text("REQUESTED DATA", style = MaterialTheme.typography.titleLarge) Card(Modifier.padding(vertical = 8.dp)) { Document(requested.docType, requested.namespaces.value.toUi()) } Spacer(Modifier.padding(12.dp)) Text("MATCHED CREDENTIALS", style = MaterialTheme.typography.titleLarge) Spacer(Modifier.padding(6.dp)) matchedCredentials.forEach { matchedCredential -> // Step 3.2: Display matching credentials and claims } // Step 4.1: Send response } } // Step 3.1: Create function to add values to claims private fun Map<NameSpace, DataElements>.toUi() = mapValues { (_, dataElements) -> dataElements.value.keys.toSet() }
This code is very similar to the one used in the in the
PresentationSelectCredentials.kt
file in the Proximity
Presentation
tutorial, to avoid creating dependencies between the tutorials. In your own
project you can use the same components for both presentation workflows.
-
Add the following code under the
// Step 2.1: Create an online presentation session
comment to create a new online presentation session when theOnlinePresentationScreen
composable appears on the screen:OnlinePresentationScreen.kt LaunchedEffect(requestUri) { withContext(Dispatchers.IO) { val mdocHolder = MobileCredentialHolder.getInstance() while (!mdocHolder.initialized) delay(100) session = mdocHolder .createOnlinePresentationSession(requestUri, requireTrustedVerifier = false) } }
This calls the SDK's
createOnlinePresentationSession
function, passing requestUri
as an argument, which is the Authorization request URI retrieved from
the deep link/QR code. The function returns an
OnlinePresentationSession
object which is stored in the declared session
variable.
This object includes a matchedCredentials
property, which details the requested information
(MobileCredentialRequest)
and any existing credentials that match it
(MobileCredentialMetaData).
We will use this information in the next steps to display this information to the user.
We must wait for the SDK instance to be initialized, because in this tutorial the SDK initialization
is called in a coroutine in the MainActivity.onCreate
method.
We chose to set requireTrustedVerifier
parameter to false
because we want
the SDK to trust all verifiers by default. If you require to interact with a
limited list of verifiers, you may want to manually add trusted verifier
certificates and set the parameter to true
. You can learn more about
certificate management in our SDK
docs.
-
In your
MainActivity.kt
file, add the following code under the// Online Presentation - Step 2.2: Add "Online Presentation" screen call
comment to connect the createdOnlinePresentationScreen
composable to the navigation graph:MainActivity.kt composable( "onlinePresentation", deepLinks = listOf( navDeepLink { uriPattern = "mdoc-openid4vp://{wildcard}" } ) ) { @Suppress("DEPRECATION") val deepLink = it.arguments ?.getParcelable<Intent>(NavController.KEY_DEEP_LINK_INTENT) ?.data ?.toString() ?: "" OnlinePresentationScreen(this@MainActivity, deepLink) }
Previously you
added an intent filter
for deep links with a
mdoc-openid4vp
scheme, so that the app is started when the intent with the deep link is filtered.
Now, when the app is opened via this deep link, it will also start the OnlinePresentationScreen
composable, and pass the deep link as the requestUri
argument.
-
In your project's
app
directory, create a new file namedonline-presentation.tsx
and add the following scaffolding code:app/online-presentation.tsx // Step 3.2: Import Credential selector component import { useHolder } from "@/providers/HolderProvider"; import { type OnlinePresentationSession, createOnlinePresentationSession, } from "@mattrglobal/mobile-credential-holder-react-native"; import { useGlobalSearchParams, useRouter } from "expo-router"; import React, { useState, useEffect, useCallback } from "react"; import { Alert, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; export default function OnlinePresentation() { const router = useRouter(); const { scannedValue: authorisationRequestUri } = useGlobalSearchParams<{ scannedValue: string; }>(); const { isHolderInitialised } = useHolder(); const [onlinePresentationSession, setOnlinePresentationSession] = useState<OnlinePresentationSession | null>(null); const [error, setError] = useState<string | null>(null); const [requests, setRequests] = useState< OnlinePresentationSession["matchedCredentials"] >([]); const [selectedCredentialIds, setSelectedCredentialIds] = useState< string[] >([]); const handleError = useCallback((message: string) => { setError(message); console.error(message); }, []); // Step 3.3: Add handleToggleSelection function // Step 2.6: Create Online Presentation Session // Step 4.1: Add handleSendResponse function if (error) { return ( <View style={styles.container}> <Text style={styles.errorText}>Error: {error}</Text> </View> ); } if (!onlinePresentationSession) { return ( <View style={styles.container}> <Text>No online presentation session</Text> </View> ); } // Step 3.4: Display presentation session details } const styles = StyleSheet.create({ container: { flex: 1, padding: 16, }, errorText: { color: "red", fontSize: 16, }, button: { backgroundColor: "#007AFF", paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8, alignItems: "center", marginTop: 20, }, buttonText: { color: "white", fontSize: 16, fontWeight: "600", }, verifierSection: { backgroundColor: "#f0f0f0", padding: 10, borderRadius: 8, marginBottom: 15, }, label: { fontWeight: "bold", fontSize: 14, textTransform: "uppercase", marginBottom: 5, }, verifierText: { fontSize: 16, }, });
-
Open the
app/index.tsx
file and add the following code inside thehandleScanComplete
function under the// Online Presentation - Step 2.2: Handle the 'mdoc-openid4vp://' scheme prefix
comment:app/index.tsx else if (scannedValue.startsWith('mdoc-openid4vp://')) { router.replace({ pathname: '/online-presentation', params: { scannedValue } }) }
The application will now redirect to the online-presentation
screen whenever a QR code which
includes a link prefixed with mdoc-openid4vp://
is scanned. This handles invoking the correct
screen in cross-device workflows.
Now, we need to make sure the application navigates to the online-presentation
screen when
following deep links that are prefixed with mdoc-openid4vp://
as part of same-device workflows.
Add the following code to the HolderProvider
component to handle linking:
- Open the
app/providers/HolderProvider.tsx
and add the following code under the// Online Presentation - Step 2.3: Import expo-linking and expo-router
to import the required redirect components:
import * as Linking from "expo-linking";
import { useRouter } from "expo-router";
- Add the following code under the
// Online Presentation - Step 2.4: Initialize router variable
comment to use the importeduseRouter
component:
const router = useRouter();
- Add the following code under the
// Online Presentation - Step 2.5: Handle deep link
comment to use therouter
component redirect the user to theonline-presentation
screen when following a deep link prefixed withmdoc-openid4vp://
:
useEffect(() => {
if (!isHolderInitialised) return;
const handleDeepLink = (event: { url: string }) => {
const { url } = event;
console.log("Deep link received:", url);
if (url.startsWith("mdoc-openid4vp://")) {
router.replace({
pathname: "/online-presentation",
params: { scannedValue: url },
});
}
};
Linking.getInitialURL().then((url) => {
if (url) {
console.log("Initial URL:", url);
handleDeepLink({ url });
}
});
const subscription = Linking.addEventListener("url", handleDeepLink);
return () => subscription.remove();
}, [isHolderInitialised, router]);
This function was created in the Claim a credential tutorial to handle QR codes which include OID4VCI credential offers.
-
Return to the
online-presentations.tsx
file and add the following code under the// Step 2.6: Create Online Presentation Session
comment to create a function that calls the SDK'screateOnlinePresentationSession
function with theauthorizationRequestURI
parameter (the request URI retrieved from the deep link/QR code) to create anOnlinePresentationSession
instance and assign it to thesession
variable:app/online-presentation.tsx useEffect(() => { if (!isHolderInitialised || !authorisationRequestUri) return; const createSession = async () => { try { const result = await createOnlinePresentationSession({ authorisationRequestUri, requireTrustedVerifier: false, }); if (result.isErr()) { throw new Error("Error creating presentation session"); } const session = result.value; setOnlinePresentationSession(session); if (session.matchedCredentials) { setRequests(session.matchedCredentials); } } catch (err: any) { handleError(err.message); } }; createSession(); }, [isHolderInitialised, authorisationRequestUri, handleError]);
Once the result
(the response returned by the
createOnlinePresentationSession
function) is available, your application can access its matchedCredentials
object, which contains
the list of credentials that match the verifier's request. Your application will use this
information to display the request details and allow the user to select which credentials to share.
We chose to set requireTrustedVerifier
parameter to false
because we want
the SDK to trust all verifiers by default. If you require to interact with a
limited list of verifiers, you may want to manually add trusted verifier
certificates and set this parameter to true
. You can learn more about
certificate management in our SDK
docs.
-
Open the
app/index.tsx
file and add the following code under the{/* Online Presentation - Step 2.7: Add Online Presentation button */}
comment to add a button that will open the scanner and enable the user to scan a QR code and start an online presentation session:app/index.tsx <TouchableOpacity style={styles.button} onPress={() => setIsScannerVisible(true)} > <Text style={styles.buttonText}>Online Presentation</Text> </TouchableOpacity>
-
Run the app for your targeted device (using
yarn android --device
and/oryarn ios --device
to rebuild the deleted folders) and perform the following instructions:- Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
- Select any of the mDL options from the request templates list.
- Select Request credentials.
- Open the camera on your testing mobile device and scan the QR code.
- Confirm opening the QR code with your tutorial application.
- The tutorial application should be launched on your testing mobile device.
You have not yet implemented the logic to handle the presentation request, so the application will not display any information at this stage. Let's proceed to the next step to fix that.
Step 3: Handle a presentation request
We will now build the capability to use information retrieved by the
createOnlinePresentationSession
function to handle the presentation request. This includes:
- Displaying what information is requested.
- Displaying what existing credentials match the requested information.
- Displaying what information from these existing claims will be shared with the verifier.
- Asking for the user's consent to share requested information from matching credentials.
The following two steps are also included in the Proximity presentation tutorial. If you had already completed this tutorial you may skip to step 3.
-
Replace the
print
statement under the// Proximity and Online Presentation: Retrieve a credential from storage
comment with the following code create a function that uses the SDK's getCredential method to retrieve a credential from the application storage:ContentView Task { do { let credential = try await mobileCredentialHolder.getCredential(credentialId: id) matchedCredentials.append(credential) } catch { print(error) } }
The MobileCredentialMetadata object does not include the values of claims included in the credential. To display these values, the above function calls the SDK's getCredential method with the
id
property of the MobileCredentialMetadata. -
Create a new file called
PresentCredentialsView.swift
and paste the following code to create a view to display credential requests and matching credentials stored in the application:PresentCredentialsView import MobileCredentialHolderSDK import SwiftUI struct PresentCredentialsView: View { @ObservedObject var viewModel: PresentCredentialsViewModel @State var selectedID: String? init(viewModel: PresentCredentialsViewModel) { self.viewModel = viewModel } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { Text("Requested Documents") .font(.headline) .padding(.leading) ForEach(viewModel.requestedDocuments, id: \.docType) { requestedDocument in DocumentView(viewModel: DocumentViewModel(from: requestedDocument)) } Text("Matched Credentials") .font(.headline) .padding(.leading) ForEach(viewModel.matchedMetadata, id: \.id) { matchedMetadata in VStack(alignment: .leading, spacing: 10) { if let matchedCredential = viewModel.matchedMobileCredential(id: matchedMetadata.id) { DocumentView(viewModel: DocumentViewModel(from: matchedCredential)) .padding(.vertical) .background(selectedID == matchedMetadata.id ? Color.blue.opacity(0.2) : Color.clear) .onTapGesture { guard selectedID != matchedMetadata.id else { selectedID = nil return } selectedID = matchedMetadata.id } Button("Hide claim values") { viewModel.matchedCredentials.removeAll(where: { $0.id == matchedMetadata.id }) } .frame(maxWidth: .infinity, alignment: .center) } else { DocumentView(viewModel: DocumentViewModel(from: matchedMetadata)) .padding(.vertical) .background(selectedID == matchedMetadata.id ? Color.blue.opacity(0.2) : Color.clear) .onTapGesture { guard selectedID != matchedMetadata.id else { selectedID = nil return } selectedID = matchedMetadata.id } Button("Show claim values") { viewModel.getCredentialAction(matchedMetadata.id) } .frame(maxWidth: .infinity, alignment: .center) } } } } } if selectedID != nil { Button("Send Response") { viewModel.sendCredentialAction(selectedID!) } .buttonStyle(.borderedProminent) .clipShape(Capsule()) .frame(maxWidth: .infinity, alignment: .center) } } } // MARK: PresentCredentialsViewModel class PresentCredentialsViewModel: ObservableObject { @Binding var requestedDocuments: [MobileCredentialRequest] @Binding var matchedCredentials: [MobileCredential] @Binding var matchedMetadata: [MobileCredentialMetadata] var getCredentialAction: (String) -> Void var sendCredentialAction: (String) -> Void init( requestedDocuments: Binding<[MobileCredentialRequest]>, matchedCredentials: Binding<[MobileCredential]>, matchedMetadata: Binding<[MobileCredentialMetadata]>, sendCredentialAction: @escaping (String) -> Void, getCredentialAction: @escaping (String) -> Void ) { self._requestedDocuments = requestedDocuments self._matchedCredentials = matchedCredentials self._matchedMetadata = matchedMetadata self.sendCredentialAction = sendCredentialAction self.getCredentialAction = getCredentialAction } func matchedMobileCredential(id: String) -> MobileCredential? { matchedCredentials.first(where: { $0.id == id }) } }
The
PresentCredentialsView
view is used to:- Display requested information.
- Display stored credentials that include the requested information.
- Enable the user to provide consent to sharing the requested information with the verifier.
The
PresentCredentialsViewModel
object is used to reference values from a credential request. It takes two closures in its initializer:getCredentialAction: (String) -> Void
is used to display claim values.sendCredentialAction: (String) -> Void
is used to send a credential response to the verifier once the user selected a credential and provided consent by selecting the Send Response button.
-
Back in your
ContentView
file, ReplaceEmptyView
under the// Online Presentation - Step 3.3: Display online presentation view
comment with the new view that you created:
PresentCredentialsView(
viewModel: PresentCredentialsViewModel(
requestedDocuments: $viewModel.credentialRequest,
matchedCredentials: $viewModel.matchedCredentials,
matchedMetadata: $viewModel.matchedMetadata,
sendCredentialAction: viewModel.sendOnlinePresentationSessionResponse(id:),
getCredentialAction: viewModel.getCredential(id:)
)
)
- Replace the
return false
statement under the// Online Presentation - Step 3.4: View Online Presentation
comment with the following code to enable the user to manually navigate to the presentation session view if required:
onlinePresentationSession != nil
-
Run the app and then close it (this updates the app on your testing device) and perform the following instructions:
- Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
- Select any of the mDL options from the request templates list.
- Select Request credentials.
- Open the camera on your testing mobile device and scan the QR code.
- Confirm opening the QR code with your tutorial application.
- The tutorial application should be launched on your testing mobile device, displaying the verification request and any matching credentials.
- When a credential is selected, it will be highlighted and a Send Response button will appear (the logic associated with the button will be implemented in the next step).
The result will look something like this:
-
In the
OnlinePresentationScreen.kt
file, add the following code under the// Step 3.1: Create function to add values to claims
comment to create a new function that will display the values of the claims the user is about to share:OnlinePresentationScreen.kt private fun List<MobileCredentialMetaData>.withClaimValues( from: MobileCredential ): List<MobileCredentialMetaData> = map { credential -> if (credential.id != from.id) { credential } else { credential.copy( claims = credential.claims.mapValues { (namespace, claims) -> claims.map { claim -> val claimValue = from.claims[namespace]?.get(claim) claimValue?.let { "$claim: ${it.toUiString()}" } ?: claim }.toSet() } ) } }
This function retrieves all matching credentials from the MobileCredentialMetaData object and retrieves their matching values from the internal storage according to the credential's
id
. -
Add the following code under the
// Step 3.2: Display matching credentials and claims
comment to display to the user what credentials and claims they are about to share with the verifier, as well as a button that enables the user to display the value of these claims:OnlinePresentationScreen.kt val borderWidth = if (matchedCredential.id == selectedCredentialId) 4.dp else 0.dp Column( Modifier .clickable { selectedCredentialId = matchedCredential.id } .border(borderWidth, Color.Blue, RoundedCornerShape(16.dp)) .padding(8.dp) ) { Card(Modifier.fillMaxWidth()) { Document(matchedCredential.docType, matchedCredential.claims) } Button( onClick = { val credentialWithValues = MobileCredentialHolder.getInstance() .getCredential(matchedCredential.id, skipStatusCheck = true) matchedCredentials = matchedCredentials.withClaimValues(from = credentialWithValues) }, Modifier.fillMaxWidth() ) { Text("Show Values") } } Spacer(Modifier.padding(12.dp))
-
Run the app and then close it (this updates the app on your testing device) and perform the following instructions:
- Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
- Select any of the mDL options from the request templates list.
- Select Request credentials.
- Open the camera on your testing mobile device and scan the QR code.
- Confirm opening the QR code with your tutorial application.
- The tutorial application should be launched on your testing mobile device, displaying the verification request and any matching credentials.
The result will look something like this:
First you will create a component that will display the requested information and allow the user to select which credentials to share.
The following step is also included in the Proximity presentation
tutorial.
If you had already completed this tutorial and created the
RequestCredentialSelector.tsx
component you may skip to step 3.2.
-
In the
app/components
directory, create a new file namedRequestCredentialSelector.tsx
and add the following code:app/components/RequestCredentialSelector.tsx import type { MobileCredentialMetadata, PresentationSessionSuccessRequest, } from "@mattrglobal/mobile-credential-holder-react-native"; import type React from "react"; import { FlatList, type ListRenderItem, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; type RequestCredentialSelectorProps = { requests: PresentationSessionSuccessRequest["request"]; selectedCredentialIds: string[]; onToggleSelection: (credentialId: string) => void; }; type RequestItem = PresentationSessionSuccessRequest["request"][number]; /** * Component that renders a list of credential requests and their matched credentials. * * @param props - The component props. * @param props.requests - The list of credential requests. * @param props.selectedCredentialIds - The list of selected credential IDs. * @param props.onToggleSelection - Callback function to toggle the selection of a credential. * @returns The rendered component. */ export default function RequestCredentialSelector({ requests, selectedCredentialIds, onToggleSelection, }: RequestCredentialSelectorProps) { const renderCredential: ListRenderItem<MobileCredentialMetadata> = ({ item: cred, }) => { const isSelected = selectedCredentialIds.includes(cred.id); return ( <TouchableOpacity style={styles.credentialItem} onPress={() => onToggleSelection(cred.id)} > <View style={styles.selectionIndicator}> {isSelected && <View style={styles.selectionInner} />} </View> <Text style={styles.credentialText}> {cred.branding?.name ?? "Credential"} ({cred.id}) </Text> </TouchableOpacity> ); }; const renderRequest: ListRenderItem<RequestItem> = ({ item }) => ( <View style={styles.requestContainer}> <Text style={styles.label}>Request Details</Text> <Text style={styles.requestInfo}> {typeof item.request === "object" ? JSON.stringify(item.request, null, 2) : item.request} </Text> <Text style={styles.label}>Matched Credentials:</Text> <FlatList data={item.matchedCredentials} keyExtractor={(cred) => cred.id} renderItem={renderCredential} style={styles.credentialsList} contentContainerStyle={styles.credentialsListContent} /> </View> ); return ( <FlatList data={requests} keyExtractor={(_, idx) => idx.toString()} renderItem={renderRequest} style={styles.requestsList} contentContainerStyle={styles.requestsListContent} /> ); } const styles = StyleSheet.create({ requestsList: { flex: 1, }, requestsListContent: { paddingBottom: 10, }, requestContainer: { backgroundColor: "#f0f0f0", padding: 10, borderRadius: 8, marginBottom: 15, }, requestInfo: { fontStyle: "italic", fontSize: 12, marginBottom: 10, }, label: { fontWeight: "bold", fontSize: 14, textTransform: "uppercase", marginBottom: 5, }, credentialsList: { maxHeight: 200, }, credentialsListContent: { paddingBottom: 10, }, credentialItem: { flexDirection: "row", alignItems: "center", marginBottom: 8, paddingVertical: 4, }, selectionIndicator: { height: 20, width: 20, borderRadius: 10, borderWidth: 1, borderColor: "#000", alignItems: "center", justifyContent: "center", marginRight: 8, }, selectionInner: { height: 10, width: 10, borderRadius: 5, backgroundColor: "#000", }, credentialText: { fontSize: 16, }, });
This component displays all existing credentials that match the verification request, and provides a UI for the user to select the credential they wish to share.
Identifiers of the selected credentials are assigned to the
selectedCredentialIds
variable, making them available for use in the next steps. -
Open the
online-presentation.tsx
file and add the following code under the// Step 3.2: Import Credential selector component
comment to import theRequestCredentialSelector
component created in the previous step:app/online-presentation.tsx import RequestCredentialSelector from "@/components/RequestCredentialSelector";
Before we can display and use the RequestCredentialSelector
component, we need to implement a
functionality that allows users to select which credentials they want to share.
The handleToggleSelection
function updates the selectedCredentialIds
state array when users tap
on credentials. This function is passed to the RequestCredentialSelector
component to handle
selection state, and the resulting array of selected credential identifiers will be used when
sending the presentation response to the verifier.
- Add the following code under the
// Step 3.3: Add handleToggleSelection function
comment to create thehandleToggleSelection
function:
const handleToggleSelection = useCallback((id: string) => {
setSelectedCredentialIds(
(prev) =>
prev.includes(id)
? prev.filter((item) => item !== id) // Remove if already selected
: [...prev, id], // Add if not selected
);
}, []);
- Add the following code under the
// Step 3.4: Display presentation session details
comment to display the request details and theRequestCredentialSelector
component, passing in the required props:
return (
<View style={styles.container}>
{/* Display verifier information */}
<View style={styles.verifierSection}>
<Text style={styles.label}>Verifier:</Text>
<Text style={styles.verifierText}>
{onlinePresentationSession.verifiedBy.value}
</Text>
</View>
{/* Component to select credentials for the presentation */}
<RequestCredentialSelector
requests={requests}
selectedCredentialIds={selectedCredentialIds}
onToggleSelection={handleToggleSelection}
/>
{/* Step 4.2: Add Send response button */}
</View>
);
-
Run the app and perform the following instructions:
- Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
- Select any of the mDL options from the request templates list.
- Select Request credentials.
- Open the camera on your testing mobile device and scan the QR code.
- Confirm opening the QR code with your tutorial application.
- The tutorial application should be launched on your testing mobile device.
- The tutorial application should display the verification request and any matching credentials, enabling the user to select any specific credential for sharing.
The result will look something like this:
Step 4: Send response
After displaying matching credentials to the user and enabling them to select what credential to share, the last thing you need to do is build the capability to share the selected credential with the verifier.
- Replace the
print
statement under the// Online Presentation - Step 4.1: Send online presentation response
comment with the following code to call thesendResponse
method when the user selects the Send Response button:
Task {
do {
_ = try await onlinePresentationSession?.sendResponse(credentialIds: [id])
// set presentation session to nil after sending a response
onlinePresentationSession = nil
// Return to root view after the response is sent
navigationPath = NavigationPath()
} catch {
print(error)
}
}
-
Add the following code under the
// Step 4.1: Send response
comment to add aSend Response
button, that will call the SDK's sendResponse function and send the selected credential to the Verifier, when pressed:OnlinePresentationScreen.kt Button( onClick = { coroutineScope.launch { session?.sendResponse(listOf(selectedCredentialId), activity) } }, Modifier.fillMaxWidth() ) { Text("Share information") }
-
Add the following code under the
// Step 4.1: Add handleSendResponse function
comment to create a function that calls thesendResponse
method of the SDK'sonlinePresentationSession
object and sends the selected credential to the verifier:app/online-presentation.tsx const handleSendResponse = useCallback(async () => { if (!onlinePresentationSession) return; if (selectedCredentialIds.length === 0) { Alert.alert( "No Credential Selected", "Please select at least one credential first.", ); return; } try { const sendResponseResult = await onlinePresentationSession.sendResponse({ credentialIds: selectedCredentialIds, }); if (sendResponseResult.isErr()) { throw new Error("Failed to send presentation response"); } router.replace("/"); Alert.alert("Success", "Presentation response sent successfully!"); } catch (err: any) { handleError(err.message); Alert.alert( "Error", "Failed to send presentation response. Terminating session...", ); await onlinePresentationSession.terminateSession(); } }, [onlinePresentationSession, selectedCredentialIds, router, handleError]);
-
Add the following code under the
{/* Step 4.2: Add Send response button */}
comment to create a button that will enable the user to send a response to the verifier after selecting which credentials to share:app/online-presentation.tsx <TouchableOpacity style={styles.button} onPress={handleSendResponse}> <Text style={styles.buttonText}>Send Response</Text> </TouchableOpacity>
Step 5: Test the application
Let's test that the application is working as expected in both workflows.
Same-device workflow
- Run the app and then close it (this updates the app on your testing device).
- Use a browser on your testing mobile device to navigate to the MATTR Labs remote presentation testing tool.
- Select any of the mDL options from the request templates list.
- Select Request credentials.
- Select Allow to open the tutorial application.
- The tutorial application should be launched on your testing mobile device.
- Select the credential you wish to send to the verifier from the list of matched credentials.
- Select Send Response.
- You should be redirected back to the MATTR Labs remote presentation testing tool, where you will see a successful verification indication.
The result will look something like this:
Cross-device workflow
- Run the app and then close it (this updates the app on your testing device).
- Use a desktop browser to navigate to the MATTR Labs remote presentation testing tool.
- Select any of the mDL options from the request templates list.
- Select Request credentials.
- Open the camera on your testing mobile device and scan the QR code.
- Confirm opening the QR code with your tutorial application.
- The tutorial application should be launched on your testing mobile device.
- Select the credential you wish to send to the verifier from the list of matched credentials.
- Select Send Response.
- Back on your desktop browser, you should see a successful verification indication.
The result will look something like this:
Summary
You have just used the mDocs Holder SDKs to build an application that can present a claimed mDoc to a verifier remotely via an online presentation workflow as per ISO/IEC 18013-7:2025 and OID4VP.
This was achieved by building the following capabilities into the application:
- Handle an OID4VP request URI.
- Create an online presentation session.
- Handle a presentation request.
- Send a presentation response.
What's next?
- You can build additional capabilities into your new application:
- Present a claimed mDoc for verification via a proximity presentation workflow.
- You can build a web application that will interact with your wallet application via an online verification workflow.
- You can check out the SDKs reference documentation for more details on the available functions and classes:
How would you rate this page?