Learn how to build an application that can present an mDoc via a proximity workflow
Introduction
In this tutorial you will use the mDocs Holder SDKs to build an application that can present a claimed mDoc to a verifier that supports proximity verification as per ISO/IEC 18013-5.
- The user launches the wallet application and generates a QR code.
- The verifier scans the QR code, connects with the wallet and requests an mDoc for verification.
- The wallet displays matching credentials to the user and asks for consent to share them with the verifier.
- The verifier receives the wallet's response and verifies the provided credential.
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 the ISO/IEC 18013-5 standard. If you are unfamiliar with this standard, refer to the following resources for more information:
- What are mDocs?
- What is credential verification?
- Breakdown of the proximity 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.
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 extension.
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
As this tutorial implements a proximity presentation workflow, you will need two separate physical devices to test the end-to-end result:
- Holder device:
- Supported iOS device to run the built application on, setup with:
- Biometric authentication.
- Bluetooth access.
- Available internet connection.
- Supported iOS device to run the built application on, setup with:
- Verifier device:
- Android/iOS device with an installed verifier application. We recommend downloading and using the MATTR GO Verify example app.
- Setup with Bluetooth access.
- Holder device:
- Supported Android device to run the built application on, setup with:
- Biometric authentication (Face recognition, fingerprint recognition).
- Bluetooth access and Bluetooth turned on.
- Available internet connection.
- USB debugging enabled.
- Supported Android device to run the built application on, setup with:
- Verifier device:
- Android/iOS mobile device with an installed verifier application. We recommend downloading and using the MATTR GO Verify example app.
- Setup with Bluetooth access.
- Holder device:
- Supported iOS and/or Android device to run the built application on, setup with:
- Biometric authentication.
- Bluetooth access and Bluetooth turned on.
- Available internet connection.
- USB debugging enabled (Android only).
- Supported iOS and/or Android device to run the built application on, setup with:
- Verifier device:
- Android/iOS device with an installed verifier application. We recommend downloading and using the MATTR GO Verify example app.
- Setup with Bluetooth access and Bluetooth turned on.
Got everything? Let's get going!
Tutorial steps
To enable a user to present a stored mDoc to a verifier via a proximity presentation workflow, you will build the following capabilities into your wallet application:
- Create a QR code for the verifier to scan and establish a secure connection.
- Receive and handle a presentation request from the verifier.
- Send a matching mDoc presentation to the verifier.
Step 1: Create a QR code for the verifier to scan
The first capability you need to build is to establish a secure communication channel between the verifier and holder devices. As defined in ISO/IEC 18130-5:2021, a proximity presentation workflow is always initiated by the holder (wallet user), who must create a QR code for the verifier to scan in order to initiate the device engagement phase.
To achieve this, your wallet application needs a UI element for the user to interact with and
trigger device engagement by calling the SDK's createProximityPresentationSession
method.
-
Open the project that you built as part of the Claim a credential tutorial.
-
Open the
ContentView
file and add the following code under the// Proximity Presentation - Step 1.2: Create deviceEngagementString and proximityPresentationSession variables
comment to create new variables to hold the device engagement string and the proximity presentation session.ContentView @Published var deviceEngagementString: String? @Published var proximityPresentationSession: ProximityPresentationSession?
-
Replace the
print
statement under the// Proximity Presentation - Step 1.3: Create function to create a proximity presentation session and generate QR code
comment with the following code to call the SDK'screateProximityPresentationSession
method when the user selects the Create QR Code button:ContentView Task { @MainActor in do { proximityPresentationSession = try await mobileCredentialHolder.createProximityPresentationSession( onRequestReceived: onRequestReceived(_:error:) ) deviceEngagementString = proximityPresentationSession?.deviceEngagement } catch { print(error) } }
At this stage the project won't compile because you need to update the
signature of the func onRequestReceived
.
-
Replace the
func
statement below the// Proximity Presentation - Step 1.4: Update function signature
comment with the following code to update the function's signature (don't change the function body for now):ContentView func onRequestReceived(_ mobileCredentialRequests: [(request: MobileCredentialRequest, matchedMobileCredentials: [MobileCredentialMetadata])]?, error: Error?) {
-
Replace the
EmptyView()
statement under the// Proximity Presentation - Step 1.5: Add button to generate QR code
comment and add the following code to create a button that will generate the QR code when the user selects it:ContentView Button { viewModel.createDeviceEngagementString() // Navigates user to presentCredentialsView, once the string has been created. viewModel.navigationPath.append(NavigationState.presentCredentials) } label: { Text("Present Credentials") }
Now, when the user selects the Create QR Code button, the application will call the SDK's
createProximityPresentationSession
method, which returns a
ProximityPresentationSession
instance that includes a deviceEngagement
string in base64
format:
"mdoc:owBjMS4wAYIB2BhYS6QBAiABIVgghaBYJe7KSqcEolhmnIJaYJ2AIevkKbEy5xP7tkwlqAwiWCAMGCGe6uFI2hKeghb59h_K4hPV-Ldq6vnaxsRiySMH9gKBgwIBowD0AfULUKRoj0ZH60Qco-m0k97qRSQ"
The deviceEngagement
string is always prefixed with mdoc:
and contains the information required
to establish a secure connection between the two devices, including:
- Wireless communication protocols supported by the holder.
- How the verifier can connect with the holder.
- Ephemeral public key which is unique to the current session.
- Additional feature negotiations.
Your app needs to convert this deviceEngagement
string into a QR code and display it in the wallet
UI for the verifier to scan.
-
Replace the
return nil
statement under the// Proximity Presentation - Step 1.6: Generate QR code
comment with the following code to retrieve data from thedeviceEngagement
string and convert it into a QR code:ContentView guard let filter = CIFilter(name: "CIQRCodeGenerator") else { return nil } filter.setValue(data, forKey: "inputMessage") guard let ciimage = filter.outputImage else { return nil } let transform = CGAffineTransform(scaleX: 10, y: 10) let scaledCIImage = ciimage.transformed(by: transform) let uiimage = UIImage(ciImage: scaledCIImage) return uiimage.pngData()
-
Replace the
EmptyView()
statement under the// Proximity Presentation - Step 1.7: Create QR code view
comment with the following code to generate a QR Code and display it to the user:ContentView VStack { Text("Scan to establish device engagement session") .font(.title3) Spacer() if let imageData = generateQRCode(data: viewModel.deviceEngagementString?.data(using: .utf8) ?? Data()), let image = UIImage(data: imageData) { Image(uiImage: image) .resizable() .aspectRatio(contentMode: .fit) } Spacer() }
-
Run the app and select the Present Credentials button. You should see a result similar to the following:
- As the user selects the Present Credentials button, they are navigated to the new view, and a new proximity presentation session is created.
- Once the session is created, the application generates and displays a QR code that can be scanned by a verifier device to establish a secure proximity communication channel over Bluetooth.
- Once the QR code is displayed, the
createProximityPresentationSession
function enters a listening state, ready to establish a Bluetooth connection with a verifier application that scans the QR Code. - When a verifier application scans the QR code, the devices will automatically exchange public keys
to establish a secure communication channel, enabling the verifier to send a presentation request,
which details:
- What credentials are required.
- What specific claims are required from these credentials.
-
In your Claim a tutorial application project, create a new file named
PresentationQrScreen.kt
and add the following code:PresentationQrScreen.kt import android.app.Activity import android.graphics.Bitmap import android.graphics.Color import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntSize import androidx.navigation.NavController import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.journeyapps.barcodescanner.BarcodeEncoder import global.mattr.mobilecredential.holder.MobileCredentialHolder import global.mattr.mobilecredential.holder.ProximityPresentationSession import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @Composable fun PresentationQrScreen(activity: Activity, navController: NavController) { var containerSize by remember { mutableStateOf(IntSize.Zero) } var session: ProximityPresentationSession? by remember { mutableStateOf(null) } var qrCode: Bitmap? by remember { mutableStateOf(null) } LaunchedEffect(Unit) { // Step 1.4: Create a Proximity presentation session } LaunchedEffect(session, containerSize) { // Step 1.6: Generate a QR code } Box( modifier = Modifier .aspectRatio(1f) .onSizeChanged { containerSize = it } ) { qrCode?.let { Image( bitmap = it.asImageBitmap(), contentDescription = "A QR Code", modifier = Modifier.fillMaxSize() ) } } } // Step 1.5: Create function to generate QR Code from String
We will copy and paste different code snippets into specific locations in this codebase to achieve the different functionalities. These locations are indicated by comments that reference both the section and the step.
We recommend copying and pasting the comment text (e.g. // Proximity Presentation - Step 1.2: Add "QR" screen call
) to
easily locate it in the code.
-
Return to your
MainActivity
file and add the following code under the// Proximity Presentation - Step 1.2: Add "QR Presentation" screen call
comment to connect the created composable to the navigation graph:MainActivity.kt PresentationQrScreen(this@MainActivity, navController)
-
Still in the
MainActivity
file file, add the following code under the// Proximity Presentation - Step 1.3: Add button for starting the credentials presentation workflow
comment to add a button that navigates the user to thePresentationQrScreen
:MainActivity.kt Button(onClick = { navController.navigate("presentationQr") }, Modifier.fillMaxWidth()) { Text("Present Credentials") }
-
Open the
PresentationQrScreen.kt
file and add the following code under the// Step 1.4: Create a Proximity presentation session
comment to call thecreateProximityPresentationSession
function when the screen is loaded:PresentationQrScreen.kt session = MobileCredentialHolder.getInstance().createProximityPresentationSession( activity, onRequestReceived = { _, requests, e -> // Step 2.2: Handle the presentation request } )
Now, when the PresentationQrScreen
is loaded, the application will call the SDK's
createProximityPresentationSession
function, which returns a
ProximityPresentationSession
instance that includes a deviceEngagement
string in base64
format:
"mdoc:owBjMS4wAYIB2BhYS6QBAiABIVgghaBYJe7KSqcEolhmnIJaYJ2AIevkKbEy5xP7tkwlqAwiWCAMGCGe6uFI2hKeghb59h_K4hPV-Ldq6vnaxsRiySMH9gKBgwIBowD0AfULUKRoj0ZH60Qco-m0k97qRSQ"
This deviceEngagement
string is always prefixed with mdoc:
and contains the information required
to establish a secure connection between the holder and verifier devices, including:
- Wireless communication protocols supported by the holder.
- How the verifier can connect with the holder.
- Ephemeral public key which is unique to the current session.
- Additional feature negotiations.
Your application needs to convert this deviceEngagement
string into a QR code and display it in
the PresentationQrScreen
for the verifier to scan.
-
Add the following code under the
// Step 1.5: Create function to generate QR Code from String
comment to add a function that converts aString
to a QR code rendered as aBitmap
image:PresentationQrScreen.kt private fun String.toQrCode(size: IntSize): Bitmap? { if (this.isEmpty() || size == IntSize.Zero) return null val (width, height) = size return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565).apply { val hints = mapOf(EncodeHintType.ERROR_CORRECTION to ErrorCorrectionLevel.M) val encoded = BarcodeEncoder() .encode(this@toQrCode, BarcodeFormat.QR_CODE, width, height, hints) for (x in 0 until width) { for (y in 0 until height) { setPixel(x, y, if (encoded[x, y]) Color.BLACK else Color.WHITE) } } } }
To generate the QR code, you need the ProximityPresentationSession
to be established by the SDK
and the container size to be calculated.
-
Add the following code under the
// Step 1.6: Generate a QR code
comment to call the newtoQrCode
function and generate the QR code when thesession
orcontainerSize
state changes:PresentationQrScreen.kt qrCode = session?.deviceEngagement?.toQrCode(containerSize)
-
Run the app, and select the Present Credentials button. You should see a result similar to the following:
- As the user selects the Present Credentials button, they are navigated to the new
PresentationQrScreen
, and a new proximity presentation session is created. - Once the session is created, the application generates and displays a QR code that can be scanned by a verifier device to establish a secure proximity communication channel over Bluetooth.
- Once the QR code is displayed, the
createProximityPresentationSession
function enters a listening state, ready to establish a Bluetooth connection with a verifier application that scans the QR Code. - When a verifier application scans the QR code, the devices will automatically exchange public keys
to establish a secure communication channel, enabling the verifier to send a presentation request,
which details:
- What credentials are required.
- What specific claims are required from these credentials.
-
In your project's
app
directory, create a new file namedproximity-presentation.tsx
and add the following scaffolding code to create a new screen that will display the generated QR code:app/proximity-presentation.tsx // Step 2.2: Import Credential selector component import { useHolder } from "@/providers/HolderProvider"; import { type PresentationSessionSuccessRequest, createProximityPresentationSession, sendProximityPresentationResponse, terminateProximityPresentationSession, } from "@mattrglobal/mobile-credential-holder-react-native"; import { useRouter } from "expo-router"; import React, { useState, useCallback, useEffect } from "react"; import { Alert, StyleSheet, Text, TouchableOpacity, View, } from "react-native"; import QRCode from "react-native-qrcode-svg"; export default function ProximityPresentation() { const router = useRouter(); const { isHolderInitialised } = useHolder(); const [error, setError] = useState<string | null>(null); const [deviceEngagement, setDeviceEngagement] = useState<string | null>( null, ); const [requests, setRequests] = useState< PresentationSessionSuccessRequest["request"] >([]); const [selectedCredentialIds, setSelectedCredentialIds] = useState< string[] >([]); const navigateToIndex = useCallback(() => { router.push("/"); }, [router]); const resetState = useCallback(() => { setDeviceEngagement(null); setRequests([]); setSelectedCredentialIds([]); }, []); const handleError = useCallback((message: string) => { setError(message); console.error(message); }, []); // Step 2.3: Add handleToggleSelection function if (!isHolderInitialised) { return ( <View style={styles.container}> <Text style={styles.errorText}> Holder instance not found. Please restart the app and try again. </Text> </View> ); } // Step 1.2: Add handleStartSession function // Step 1.6: Add terminateSession function // Step 3.1: Add handleSendResponse function return ( <View style={styles.container}> {error && <Text style={styles.errorText}>{error}</Text>} {/* Step 1.3: Add fallback UI */} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 16, paddingBottom: 32 }, errorText: { color: "red", marginBottom: 10 }, infoText: { marginBottom: 10 }, qrContainer: { alignItems: "center", marginVertical: 20 }, button: { backgroundColor: "#007AFF", paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8, alignItems: "center", marginTop: 20, }, buttonDanger: { backgroundColor: "#FF3B30", }, buttonText: { color: "white", fontSize: 16, fontWeight: "600", }, });
This will serve as the basic structure for this capability. We will copy and
paste different code snippets into specific locations in this codebase to
achieve the different functionalities. These locations are indicated by
comments that reference both the section and the step. We recommend copying
and pasting the comment text (e.g. // Proximity Presentation - Step 1.2: Add Proximity Presentation button
) to easily locate it in the code.
-
Add the following code under the
// Step 1.2: Add handleStartSession function
comment to create a newhandleStartSession
function that calls the SDK'screateProximityPresentationSession
function:app/proximity-presentation.tsx const handleStartSession = useCallback(async () => { try { const result = await createProximityPresentationSession({ onRequestReceived: (data) => { if ("error" in data) { handleError(`Request received error: ${data.error}`); return; } setRequests(data.request); }, onSessionTerminated: () => { resetState(); navigateToIndex(); }, }); if (result.isErr()) { throw new Error( `Error creating proximity session: ${JSON.stringify(result.error)}`, ); } setDeviceEngagement(result.value.deviceEngagement); } catch (err: any) { handleError(err.message); } }, [handleError, resetState, navigateToIndex]); // Automatically start session when component mounts useEffect(() => { handleStartSession(); }, [handleStartSession]);
-
Add the following code under the
{/* Step 1.3: Add fallback UI */}
comment to display a message to the user while the proximity session is being established:app/proximity-presentation.tsx { !deviceEngagement ? ( <> <Text style={styles.infoText}> Waiting for session to establish... </Text> </> ) : ( <>{/* Step 1.4: Display QR code */}</> ); }
Now, when the user navigates to the Proximity Presentation screen, the
handleStartSession
function is called and creates a new proximity presentation session by calling the SDK'screateProximityPresentationSession
function. This SDK function returns aProximityPresentationSession
instance that includes adeviceEngagement
string inBase64
format:"mdoc:owBjMS4wAYIB2BhYS6QBAiABIVgghaBYJe7KSqcEolhmnIJaYJ2AIevkKbEy5xP7tkwlqAwiWCAMGCGe6uFI2hKeghb59h_K4hPV-Ldq6vnaxsRiySMH9gKBgwIBowD0AfULUKRoj0ZH60Qco-m0k97qRSQ"
The
deviceEngagement
string is always prefixed withmdoc:
and contains the information required to establish a secure connection between the two devices, including:- Wireless communication protocols supported by the holder.
- How the verifier can connect with the holder.
- Ephemeral public key which is unique to the current session.
- Additional feature negotiations.
Your app needs to convert this
deviceEngagement
string into a QR code and display it so that the verifier can scan it. -
Add the following code under the
{/* Step 1.4: Display QR code */}
comment to use thedeviceEngagement
variable to generate a QR code and display it:app/proximity-presentation.tsx <> <Text style={styles.infoText}>A proximity session is active.</Text> {requests.length === 0 ? ( <View style={styles.qrContainer}> <QRCode value={deviceEngagement} size={200} /> {/* Step 1.7: Add Terminate session button */} </View> ) : ( <> {/* Step 2.4: Display request details */} {/* Step 3.2: Send response */} </> )} </>
-
Open the
index.tsx
file in theapp
directory and add the following code under the{/* Proximity Presentation - Step 1.5: Add Proximity Presentation button */}
comment to add a new button to navigate to the newproximity-presentation
screen created in the previous steps:app/index.tsx <TouchableOpacity style={styles.button} onPress={() => router.replace("/proximity-presentation")} > <Text style={styles.buttonText}>Proximity Presentation</Text> </TouchableOpacity>
Proximity verification workflows require the use of Bluetooth to establish a secure communication channel with a verifier device. To enable this feature for iOS applications, you need to adjust the app configuration to include the necessary permissions.
To properly manage proximity sessions, we will create two functions that allow the application to gracefully handle scenarios where users navigate away from the screen before completing the verification process.
-
Add the following code under the
{/* Proximity Presentation - Step 1.7: Add terminateSession function */}
comment to create theterminateSession
andhandleTerminateSession
functions:app/proximity-presentation.tsx const terminateSession = useCallback(async () => { try { await terminateProximityPresentationSession(); resetState(); navigateToIndex(); } catch (err: any) { handleError(`Failed to terminate session: ${err.message}`); } }, [isHolderInitialised, resetState, handleError, navigateToIndex]); const handleTerminateSession = useCallback(async () => { await terminateSession(); }, [terminateSession]); useEffect(() => { return () => { if (deviceEngagement) { terminateSession(); } }; }, [deviceEngagement, terminateSession]);
-
Add the following code under the
{/* Proximity Presentation - Step 1.7: Add Terminate session button */}
comment to create a button that will call thehandleTerminateSession
function when selected:app/proximity-presentation.tsx <TouchableOpacity style={[styles.button, styles.buttonDanger]} onPress={handleTerminateSession} > <Text style={styles.buttonText}>Terminate Session</Text> </TouchableOpacity>
The
handleTerminateSession
function is called when the user selects the Terminate Session button, and it displays an alert to confirm that the session has been terminated.The
useEffect
hook acts as a cleanup function when the component is unmounted or if the user navigates away from the screen. If a proximity session is active, it calls theterminateSession
function to end the session.The
terminateSession
function calls the SDK'sterminateProximityPresentationSession
method to end the current proximity presentation session and reset the application state. -
Run the app and select the Proximity Presentation button. You should see a result similar to the following:
-
As the user selects the Proximity Presentation button a new proximity presentation session is created.
-
Once the session is created, the application retrieves the
deviceEngagement
string and uses it to generate and display a QR code that can be scanned by a verifier device to establish a secure proximity communication channel over Bluetooth. -
Once the QR code is displayed, the
createProximityPresentationSession
function enters a listening state, ready to establish a Bluetooth connection with a verifier application that scans the QR Code. -
When a verifier application scans the QR code, the devices will automatically exchange public keys to establish a secure communication channel, enabling the verifier to send a presentation request, which details:
- What credentials are required.
- What specific claims are required from these credentials.
We will implement the logic that handles this presentation request in the next step.
Step 2: Handle the presentation request
The createProximityPresentationSession
function can handle three types of events:
onConnected
: When a secure connection is established.onSessionTerminated
: When a secure connection is terminated for whatever reason.onRequestReceived
: When a presentation request is received from the verifier.
onConnected
and onSessionTerminated
are optional events and will not be
implemented in this tutorial. You can find more information about these events
in the reference documentation for the
iOS,
Android
and React
Native
SDKs.
When the SDK receives a presentation request from a verifier, an onRequestReceived
event is
triggered. The SDK then checks its credential storage for any credentials that match the information
defined in this request.
The application then needs to:
- Present these matching credentials to the user.
- Present what claims will be shared with the verifier.
- Provide a UI element for the user to consent sharing this information with the verifier.
The following step is also included in the Online presentation tutorial. If you had already completed this tutorial you may skip to step 2.
-
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.
Once the application receives a credential request from a verifier (onRequestReceived
event), the
createProximityPresentationSession
function will return a mobileCredentialRequests
array that includes pairs of credentials requested
by the verifier
(MobileCredentialRequest)
and the metadata of stored credentials that match the requested information
(MobileCredentialMetadata).
- Replace the
print
statements under the// Proximity Presentation - Step 2.2: Store credential requests and matched credentials
comment with the following code to store the values from themobileCredentialRequests
array in thematchedMetadata
andcredentialRequest
variables:
Task { @MainActor in
matchedMetadata = mobileCredentialRequests?
.flatMap { $0.matchedMobileCredentials }
.compactMap { $0 } ?? []
credentialRequest = mobileCredentialRequests?
.compactMap { $0.request }
.compactMap { $0 } ?? []
// Navigate to presentation view if there are no errors
if error == nil {
navigationPath.append(NavigationState.proximityPresentation)
} else {
print(error!)
}
}
The following two steps are also included in the Online presentation tutorial. If you had already completed this tutorial you may skip to step 5.
-
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.
-
Return to the
ContentView
file and replace theEmptyView()
statement under the// Proximity Presentation - Step 2.5: Display proximity presentation view
comment with the new view that you created:ContentView PresentCredentialsView( viewModel: PresentCredentialsViewModel( requestedDocuments: $viewModel.credentialRequest, matchedCredentials: $viewModel.matchedCredentials, matchedMetadata: $viewModel.matchedMetadata, sendCredentialAction: viewModel.sendProximityPresentationResponse(id:), getCredentialAction: viewModel.getCredential(id:) ) )
-
Run the app, select the Present Credentials button and then select the Create QR code button. Next, use your testing verifier app to scan the presented QR code and send a presentation request. You should see a result similar to the following:
As the user selects the Present Credentials button, the wallet generates and displays a QR code.
When a compliant verifier app scans the QR code, a secure communication channel is established via Bluetooth. The verifier then sends a presentation request, which is displayed to the user on their digital wallet, alongside any credentials they have stored in their wallet that match the request.
When the user selects the Show claim values button, the SDK retrieves the corresponding credential and the application displays its claim values.
When the user selects a credential, it is highlighted and the Send Response button appears at the bottom of the screen.
-
In your
MainActivity
file, add the following code under the// Step 2.1: Add proximity presentation request variable
comment to create a new variable that will hold the presentation request information and share it between the screens:MainActivity.kt var proximityPresentationRequest: ProximityPresentationSession.CredentialRequestInfo? = null
-
Open the
PresentationQrScreen.kt
file and add the following code under the// Step 2.2: Handle the presentation request
comment to navigate to a new screen when anonRequestReceived
event is received.PresentationQrScreen.kt if (e == null && !requests.isNullOrEmpty()) { // Using only the first request for simplicity SharedData.proximityPresentationRequest = requests.first() withContext(Dispatchers.Main) { navController.navigate("presentationSelectCredentials") } } else { val msg = "Error while retrieving the request" withContext(Dispatchers.Main) { Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show() } }
-
Create a new file named
PresentationSelectCredentialsScreen.kt
and add the following code to create the new screen which displays matching credentials and enables the user to select which credential to share:PresentationSelectCredentialsScreen.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.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 kotlinx.coroutines.launch @Composable fun PresentationSelectCredentialsScreen(activity: Activity) { val request = SharedData.proximityPresentationRequest ?: return var matchedCredentials by remember { mutableStateOf(request.matchedCredentials) } 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(request.request.docType, request.request.nameSpaces.value.toUi()) } Spacer(Modifier.padding(12.dp)) Text("MATCHED CREDENTIALS", style = MaterialTheme.typography.titleLarge) Spacer(Modifier.padding(6.dp)) matchedCredentials.forEach { matchedCredential -> // Step 2.5: Display matching credentials and claims } // Step 3.2: Send response } } // Step 2.4: Create function to add values to claims private fun Map<NameSpace, DataElements>.toUi() = mapValues { (_, dataElements) -> dataElements.value.keys.toSet() } // Step 3.1: Create function to send the credential response
This code is very similar to the one used in the in the
OnlinePresentationScreen.kt
file in the Online
Presentation tutorial, to avoid
creating dependencies between the tutorials. In your own project you can use
the same components for both presentation workflows.
The application retrieves information from the SDK's ProximityPresentationSession
object and
displays it to the user:
- Credentials and claims included in the verification request are retrieved from the
docType
andnameSpaces.value
properties and displayed in the "REQUESTED INFORMATION" area. - Available credentials that match the requested information are retrieved from the
matchedCredentials
property and displayed in the "MATCHING CREDENTIALS" area.
-
Add the following code under the
// Step 2.4: Create function to add values to claims
comment to create a new function that will enable displaying the values of the claims the user is about to share.PresentationSelectCredentialsScreen.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() } ) } }
-
Add the following code under the
// Step 2.5: 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 those claims:PresentationSelectCredentialsScreen.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))
-
Back in the
MainActivity
file, add the following code under the// Proximity Presentation - Step 2.6: Add "Select Credential" screen call
comment to connect the created composable to the navigation graph:MainActivity.kt PresentationSelectCredentialsScreen(this@MainActivity)
-
Run the app, and select the Present Credentials button. Next, use your testing verifier app to scan the presented QR code and send a presentation request. You should see a result similar to the following:
- As the user selects the Present Credentials button, they are navigated to the new
PresentCredentialsView
where the application generates and displays a QR code. - When a compliant verifier app scans the QR code, a secure communication channel is established via Bluetooth.
- The verifier then sends a presentation request, which is displayed to the user alongside any credentials they have stored that match the request.
The following step is also included in the Online presentation
tutorial.
If you had already completed this tutorial and created the
RequestCredentialSelector.tsx
component, you may skip to step 2.2.
- In the
app/components
directory, create a file namedRequestCredentialSelector.tsx
and add the following code:
import type {
MobileCredentialMetadata,
OnlinePresentationSession,
} 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.
- Add the following code under the
// Step 2.2: Import Credential selector component
comment in theproximity-presentation.tsx
file to import theRequestCredentialSelector
component created in the previous step:
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 2.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 2.4: Display request details */}
comment to display theRequestCredentialSelector
component to the user, enabling them to select which credentials to share:
<RequestCredentialSelector
requests={requests}
selectedCredentialIds={selectedCredentialIds}
onToggleSelection={handleToggleSelection}
/>
- Run the app, select the Proximity Presentation button and the use your verifier testing device to scan the displayed QR code. You should see a result similar to the following:
-
As the user selects the Proximity Presentation button a new proximity presentation session is created and a QR code is displayed.
-
When a verifier application scans the QR code, the devices will automatically exchange public keys to establish a secure communication channel, enabling the verifier to send a presentation request, which details:
- What credentials are required.
- What specific claims are required from these credentials.
-
The request details are then displayed by the
RequestCredentialSelector
component and enable the user to select which matching credential to share with the verifier.
We will implement the logic to share the information with the verifier in the next step.
Step 3: Send a response
The next (and final!) capability you need to build is for the wallet application to send a presentation response upon receiving consent from the user to share information with the verifier.
Once the user provides this consent by selecting the Send Response button, the wallet
application should call the SDK's sendResponse
method to share the selected credentials with the
verifier as a presentation response.
-
In the
ContentView
file replace the printstatement
under the// Proximity Presentation - Step 3.1: Send a credential response
comment with the following code to call thesendResponse
method when the user selects the Send Response button:ContentView Task { do { let _ = try await proximityPresentationSession?.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) } }
The
sendResponse
function signs the presentation response with the user’s device private key (to prove Device authentication) and shares it as an encoded CBOR file.
-
Open the
PresentationSelectCredentialsScreen.kt
file and add the following code under the// Step 3.1: Create function to send the credential response
comment to create a new function that calls the SDK'ssendResponse
method.PresentationSelectCredentialsScreen.kt private suspend fun sendResponse(credentialId: String, activity: Activity) { MobileCredentialHolder.getInstance() .getCurrentProximityPresentationSession() ?.sendResponse(listOf(credentialId), activity) }
The
sendResponse
function signs the presentation response with the user’s device private key (to prove Device authentication) and shares it as an encoded CBOR file. -
Add the following code under the
// Step 3.2: Send response
to call the createdsendResponse
function when the user selects the "Send Response" button:PresentationSelectCredentialsScreen.kt Button( onClick = { coroutineScope.launch { sendResponse(selectedCredentialId, activity) } }, Modifier.fillMaxWidth() ) { Text("Send Response") }
-
Add the following code under the
// Step 3.1: Add handleSendResponse function
comment to create a newhandleSendResponse
function that calls the SDK'ssendProximityPresentationResponse
function:app/proximity-presentation.tsx const handleSendResponse = useCallback(async () => { if (selectedCredentialIds.length === 0) { Alert.alert( "No Credentials Selected", "Please select at least one credential to send.", ); return; } try { const result = await sendProximityPresentationResponse({ credentialIds: selectedCredentialIds, }); if (result.isErr()) { await terminateSession(); throw new Error(`Error sending proximity response: ${result.error}`); } Alert.alert("Success", "Presentation response sent successfully!"); navigateToIndex(); } catch (err: any) { handleError(err.message); } }, [selectedCredentialIds, handleError, terminateSession, navigateToIndex]);
-
Add the following code under the
{/* Step 3.2: Send response */}
comment to add a new button that calls thehandleSendResponse
function created in the previous step:app/proximity-presentation.tsx <TouchableOpacity style={styles.button} onPress={handleSendResponse}> <Text style={styles.buttonText}>Share credential</Text> </TouchableOpacity>
The
sendProximityPresentationResponse
function signs the presentation response with the user’s device private key (to prove Device authentication) and shares it as an encoded CBOR file.
Step 4: Test the application
-
Run the app.
-
Select the Present Credentials button.
-
Use your testing verifier app to scan the presented QR code and send a presentation request.
-
Back on the holder device, select the matching credential to share and select the Send Response button.
You should see a result similar to the following:
As the user selects the credential to share, the verifier app will receive the presentation response, verify any incoming credentials and display the verification results.
Congratulations, you have now completed this tutorial, and should have a working wallet application that can claim an mDoc using an OID4VCI workflow, and present it to a verifier via a proximity presentation workflow.
Summary
You have just used the mDocs Holder SDKs to built an application that can present a claimed mDoc via a proximity presentation workflow as per ISO/IEC 18013-5:2021.
This was achieved by building the following capabilities into the application:
- Generating a QR code for the verifier to scan and establish a secure communication channel.
- Receive and handle a presentation request from the verifier.
- Display matching credentials to the user and ask for consent to share them with the verifier.
- Send matching credentials to the verifier as a presentation response.
What's next?
- You can build additional capabilities into your new application:
- Present a claimed mDoc for verification via an online presentation workflow into your new application.
- You can check out the SDKs reference documentation for more details on the available functions and classes:
How would you rate this page?