Learn how to build a React Native iOS and Android application that can claim an mDoc via OID4VCI
Introduction
In this tutorial you will use the React Native mDocs Holder SDK to build iOS and Android applications that can claim an mDoc issued via an OID4VCI workflow:
- The user launches the application and scans a QR code received from an issuer.
- The application displays what credential is being offered to the user and by what issuer.
- The user agrees to claiming the offered credential.
- The user is redirected to complete authentication.
- Upon successful authentication, the credential is stored by the user’s application.
The result will look something like this:
Prerequisites
Before getting started, let’s make sure you have everything you need.
Prior knowledge
-
The issuance workflow described in this tutorial is based on the OID4VCI specification. If you are unfamiliar with this specification, refer to the following Learn sections for more information:
- What is credential issuance?
- Breakdown of the OID4VCI workflow.
- What are mDocs?
-
We assume you have experience developing React Native apps.
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
- 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
- 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 .
Dependencies
All dependencies are included in the project’s package.json
file and are automatically installed
as part of the environment setup.
Testing device
- Supported iOS and/or Android device to run the built application on, setup with:
- Biometric authentication.
- Available internet connection.
Got everything? Let’s get going!
Environment setup
Perform the following steps to setup and configure your development environment.
Access the tutorial codebase
-
Access the tutorial starter codebase by either:
-
Cloning the MATTR sample-apps repository:
Clone the repositorygit clone https://github.com/mattrglobal/sample-apps.git cd sample-apps/react-native-mdocs-holder-tutorial/react-native-mdocs-holder-tutorial-starter/
or
-
Downloading just the starter directory using the download-directory.github.io utility.
-
-
Open the tutorial project in your code editor.
You can find the completed tutorial code in the react-native-mdocs-holder-tutorial-complete
directory of the same repository and use it as a reference.
iOS Application configuration
-
Open the
app.config.ts
file and update thebundleIdentifier
value under the// Update the bundle identifier
comment to a unique value for your application, e.g.com.mycompany.myapp
.app.config.tsbundleIdentifier: "com.mycompany.myapp",
iOS requires each app to have a unique bundle identifier for App Store and development environments.
-
Add the following Face ID (
NSFaceIDUsageDescription
) and camera usage (NSCameraUsageDescription
) permissions to theios.infoPlist
object under the// Add Face ID and Camera usage permissions
comment:app.config.tsNSFaceIDUsageDescription: "Face ID is used to secure your credentials.", NSCameraUsageDescription: "Camera is used to scan QR codes.",
These permissions are required for the SDK to use biometric authentication and the camera for QR code scanning.
Configure the SDK plugins
Add the following code under the // Configure the SDK plugins
comment to import required plugin
configurations:
"./withRemoveAPNSCapability.js",
[
"./withAndroidHolderSDK.js",
{
mattrDomain: "credentials",
mattrScheme: "io.mattrlabs.sample.reactnativemobilecredentialholdertutorialapp",
},
],
The SDK requires platform-specific configurations to work correctly. Three plugin files have already been created in your project root directory. You can also follow the instructions in the mDocs Holder SDK Docs to perform these platform-specific configuration manually.
Install the dependencies
Open a terminal in the project’s root and install the application dependencies:
yarn install
Generate the iOS and Android project files
Run the following command to generate the iOS and Android project files:
yarn expo prebuild
You should now see the ios
and android
folders in your project root.
Start the application
Connect your testing device(s) and run the following command to start the application(s):
iOS
yarn ios --device
reload.js
to resolve it.Android
yarn android --device
Nice work, your application is now all set to begin using the SDK!
Claiming a credential
In this part of the tutorial you will build the capability for a user to interact with an OID4VCI Credential offer and claim an mDoc.
You will break down this capability into the following steps:
- Initialize the SDK.
- Interact with a credential offer.
- Retrieve offer details and present them to the user.
- Obtain user consent and initiate credential issuance.
Initialize the SDK
The first capability you will build is to initialize the SDK so that your app can use its methods and classes. To achieve this you will create a React Context that will allow accessing an SDK instance and its helper functions throughout the application.
-
In your project’s
providers
directory, create a new file namedHolderProvider.tsx
and add the following scaffolding code:/providers/HolderProvider.tsximport { type MobileCredentialMetadata, deleteCredential, getCredentials, initialise, } from "@mattrglobal/mobile-credential-holder-react-native"; // Online Presentation - Step 2.3: Import expo-linking and expo-router import type React from "react"; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { Alert } from "react-native"; type HolderContextProps = { isHolderInitialised: boolean; getMobileCredentials: () => Promise<void>; mobileCredentials: MobileCredentialMetadata[]; deleteMobileCredential: (credentialId: string) => Promise<void>; error: string | null; isLoading: boolean; }; const HolderContext = createContext<HolderContextProps | undefined>(undefined); export function HolderProvider({ children }: { children: React.ReactNode }) { const [isHolderInitialised, setIsHolderInitialised] = useState<boolean>(false); const [mobileCredentials, setMobileCredentials] = useState<MobileCredentialMetadata[]>([]); const [error, setError] = useState<string | null>(null); const [isLoading, setIsLoading] = useState<boolean>(true); // Online Presentation - Step 2.4: Initialize router variable // Claim a Credential - Step 1.2: Initialize the Holder SDK const getMobileCredentials = useCallback(async () => { if (!isHolderInitialised) return; const credentials = await getCredentials(); setMobileCredentials(credentials); }, [isHolderInitialised]); // When the holder is initialized, get the mobile credentials to display in the app useEffect(() => { if (isHolderInitialised) { getMobileCredentials(); } }, [isHolderInitialised, getMobileCredentials]); // An example implementation of deleting a credential, used for demonstration purposes const deleteMobileCredential = useCallback(async (credentialId: string) => { if (!isHolderInitialised) return; Alert.alert("Confirm Deletion", "Are you sure you want to delete this credential?", [ { text: "Cancel", style: "cancel" }, { text: "Delete", style: "destructive", onPress: async () => { try { await deleteCredential(credentialId); Alert.alert("Success", "Credential deleted successfully."); await getMobileCredentials(); } catch (err) { console.error("Error deleting credential:", err); Alert.alert("Error", err instanceof Error ? err.message : "Failed to delete credential."); } }, },]); }, [isHolderInitialised, getMobileCredentials] ); // Online Presentation - Step 2.5: Handle deep link const contextValue = useMemo( () => ({ isHolderInitialised, error, isLoading, getMobileCredentials, mobileCredentials, deleteMobileCredential, }), [isHolderInitialised, error, isLoading, getMobileCredentials, mobileCredentials, deleteMobileCredential] ); return <HolderContext.Provider value={contextValue}>{children}</HolderContext.Provider>; } export function useHolder() { const context = useContext(HolderContext); if (context === undefined) { throw new Error("useHolder must be used within a HolderProvider"); } return context; }
This will serve as the basic structure for your application. You 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 number.
We recommend copying and pasting the comment’s text (e.g.
// Claim a Credential - Step 1.2: Initialize the Holder
) to easily locate it in the code.
-
Add the following code under the
// Claim a Credential - Step 1.2: Initialize the Holder SDK
comment to call the SDK’sinitialise
method and initialize the SDK:/providers/HolderProvider.tsxconst initialiseHolder = useCallback(async () => { const result = await initialise(); if (result.isErr()) { setError(result.error.message || "Failed to initialise holder."); } else { setIsHolderInitialised(true); } setIsLoading(false); }, []); useEffect(() => { initialiseHolder(); }, [initialiseHolder]);
-
Open the
/app/_layout.tsx
file and replace its content with the following code to wrap the application with theHolderProvider
context:_layout.tsximport { HolderProvider } from "@/providers/HolderProvider"; import { Stack } from "expo-router"; export default function RootLayout() { return ( // Claim a Credential - Step 1.3: Wrap the app in the HolderProvider component to make the HolderContext available to any child components <HolderProvider> <Stack> <Stack.Screen name="index" options={{ headerTitle: "Home", }} /> </Stack> </HolderProvider> ); }
Your application can now use the SDK’s methods and classes. Next, you will build the capability to interact with a Credential offer.
The application will now require biometric authentication whenever the SDK is initialized.
Interact with a Credential offer
Users can receive OID4VCI Credential offers as deep-links or QR codes. As this tutorial uses an offer formatted as a QR code, your application needs to be able to scan and process it.
-
Replace the code in
/app/index.tsx
with the following:/app/index.tsx// The Index component displays the list of credentials and enables claiming new credentials using a QR code scanner. import { useRouter } from "expo-router"; import React, { useState } from "react"; import { ActivityIndicator, Modal, StyleSheet, Text, TouchableOpacity, View } from "react-native"; import CredentialsList from "@/components/CredentialsList"; // Claim a Credential - Step 2.3: Import the QRCodeScanner component import { useHolder } from "@/providers/HolderProvider"; export default function Index() { const router = useRouter(); const { isHolderInitialised, error, isLoading, mobileCredentials, deleteMobileCredential } = useHolder(); const [isScannerVisible, setIsScannerVisible] = useState(false); // Claim a Credential - Step 2.4: Define the handleScanComplete function if (isLoading) { return ( <View style={styles.centered}> <ActivityIndicator size="large" color="#0000ff" /> <Text>Loading...</Text> </View> ); } if (error) { return ( <View style={styles.centered}> <Text>Error: {error}</Text> <Text>Restart the app.</Text> </View> ); } if (!isHolderInitialised) { return ( <View style={styles.centered}> <Text>No holder instance</Text> </View> ); } return ( <View style={styles.container}> <Text style={styles.headerText}>Welcome to the mDoc Holder App</Text> {/* UI to enable the QR code scanner */} <View style={styles.buttonContainer}> <TouchableOpacity style={styles.button} onPress={() => setIsScannerVisible(true)}> <Text style={styles.buttonText}>Claim Credential</Text> </TouchableOpacity> {/* Proximity Presentation - Step 1.5: Add Proximity Presentation button */} {/* Online Presentation - Step 2.7: Add Online Presentation button */} </View> {/* Display the list of credentials and their metadata */} <CredentialsList mobileCredentials={mobileCredentials} deleteMobileCredential={deleteMobileCredential} /> {/* Modal to display the QR code scanner */} {/* Claim a Credential - Step 2.5: Add the QRCodeScanner component */} </View> ); } const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 16, paddingHorizontal: 16, }, centered: { flex: 1, alignItems: "center", justifyContent: "center", }, headerText: { fontSize: 20, fontWeight: "600", marginBottom: 20, }, buttonContainer: { marginBottom: 20, gap: 10, }, button: { backgroundColor: "#007AFF", paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8, alignItems: "center", justifyContent: "center", }, buttonText: { color: "white", fontSize: 16, fontWeight: "600", }, closeButton: { marginBottom: 20, backgroundColor: "#FF3B30", }, modalContainer: { flex: 1, justifyContent: "center", }, });
-
In your project’s
components
directory, create a new file namedQRCodeScanner.tsx
and add the following code:
import React, { useRef } from 'react'
import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import {
Camera,
useCameraDevice,
useCameraPermission,
useCodeScanner
} from 'react-native-vision-camera'
type QRCodeScannerProps = {
onScanComplete: (scannedValue: string) => void
}
const { width, height } = Dimensions.get('window')
const overlaySize = width * 0.7
export default function QRCodeScanner({ onScanComplete }: QRCodeScannerProps) {
const device = useCameraDevice('back')
const { hasPermission, requestPermission } = useCameraPermission()
// Ref to track if a scan has been handled
const scanHandledRef = useRef(false)
const codeScanner = useCodeScanner({
codeTypes: ['qr'],
onCodeScanned: ([code]) => {
if (code?.value && !scanHandledRef.current) {
console.log('Scanned QR Code:', code.value)
scanHandledRef.current = true
onScanComplete(code.value)
}
}
})
// Handle cases where permissions are not granted
if (!hasPermission) {
return (
<View style={styles.centered}>
<TouchableOpacity style={styles.button} onPress={requestPermission}>
<Text style={styles.buttonText}>Request Camera Permission</Text>
</TouchableOpacity>
</View>
)
}
// Handle cases where no camera device is found
if (!device) {
return (
<View style={styles.centered}>
<Text style={styles.errorText}>No back camera device found.</Text>
</View>
)
}
return (
<View style={styles.container}>
{/* Render the Camera component */}
<Camera
device={device}
isActive={true}
style={styles.camera}
codeScanner={codeScanner}
/>
{/* QR Code Overlay */}
<View style={styles.overlayContainer}>
<View style={styles.topOverlay} />
<View style={styles.middleOverlay}>
<View style={styles.sideOverlay} />
<View style={styles.focusedArea} />
<View style={styles.sideOverlay} />
</View>
<View style={styles.bottomOverlay} />
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
camera: {
flex: 1
},
errorText: {
fontSize: 16,
color: 'red'
},
overlayContainer: {
position: 'absolute',
top: 0,
left: 0,
width: width,
height: height,
justifyContent: 'space-between',
alignItems: 'center'
},
topOverlay: {
width: width,
height: (height - overlaySize) / 2,
backgroundColor: 'rgba(0, 0, 0, 0.5)'
},
middleOverlay: {
flexDirection: 'row'
},
sideOverlay: {
width: (width - overlaySize) / 2,
height: overlaySize,
backgroundColor: 'rgba(0, 0, 0, 0.5)'
},
focusedArea: {
width: overlaySize,
height: overlaySize,
borderWidth: 2,
borderColor: 'blue',
backgroundColor: 'transparent'
},
bottomOverlay: {
width: width,
height: (height - overlaySize) / 2,
backgroundColor: 'rgba(0, 0, 0, 0.5)'
},
button: {
backgroundColor: '#007AFF',
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
alignItems: 'center'
},
buttonText: {
color: 'white',
fontSize: 16,
fontWeight: '600'
}
})
This component uses the react-native-vision-camera
library to provide QR code scanning
functionality with the following features:
- Camera Integration: Uses the device’s camera and handles permission requests.
- Visual Guidance: Displays an overlay with a focused scanning area to help users position QR codes correctly,
- Error Handling: Provides clear UI feedback when:
- Camera permissions haven’t been granted,
- No compatible camera device is found,
- Scan Processing: Captures QR code data and passes it to the parent component via the
onScanComplete
callback,
-
Next, import the
QRCodeScanner
component in theindex.tsx
file by adding the following code under the// Claim a Credential - Step 2.3: Import the QRCodeScanner component
comment:/app/index.tsximport QRCodeScanner from '@/components/QRCodeScanner'
When the QRCodeScanner
decodes the QR code, it will call the onScanComplete
callback with the
scanned value. This should be a URI that starts with openid-credential-offer://
.
-
Add the following code under the
// Claim a Credential - Step 2.4: Define the handleScanComplete function
comment to handle the scanned QR code:/app/index.tsxconst handleScanComplete = (scannedValue: string) => { setIsScannerVisible(false) if (!scannedValue) return if (scannedValue.startsWith('openid-credential-offer://')) { router.push({ pathname: '/claim-credential', params: { scannedValue } }) } // Online Presentation - Step 2.2: Handle the 'mdoc-openid4vp://' scheme prefix }
When called, the
handleScanComplete
function would redirect the user to the/claim-credential
screen with thescannedValue
parameter obtained from the QR code. This is the screen where we will display the offer details to the user and enable them to accept the offered credential.
Your code editor will likely show a warning as we will only create the /claim-credential
screen later in the tutorial.
- Add the following code under the
{/* Claim a Credential - Step 2.5: Add the QRCodeScanner component */}
comment to combine theQRCodeScanner
component with thehandleScanComplete
function:
<Modal visible={isScannerVisible} animationType="slide" onRequestClose={() => setIsScannerVisible(false)}>
<View style={styles.modalContainer}>
<QRCodeScanner onScanComplete={handleScanComplete} />
<TouchableOpacity style={[styles.button, styles.closeButton]} onPress={() => setIsScannerVisible(false)}>
<Text style={styles.buttonText}>Close Scanner</Text>
</TouchableOpacity>
</View>
</Modal>
Now, when the Claim Credential
button is selected the app will display the QRCodeScanner
component in a modal. When a QR code is successfully scanned, the onScanComplete
callback
(handleScanComplete
) is triggered, which will navigate the user to the /claim-credential
screen
with the information obtained from the QR code passed as a scannedValue
parameter.
- Run the app.
The application will now display a Claim Credential button on the home screen. As the user selects this button they will be prompted for permission to access the camera, and then the QR code scanner will be displayed.
You should see a result similar to the following:
Retrieve offer details and present them to the user
The next capability to build is for the application to display the credential offer details to the user and request their approval to claim the offered credentials. Credential discovery is the process in which a holder application retrieves the offer details, including:
- What issuer is offering the credentials?
- What credentials are being offered, in what format and what claims do they include?
To display this information to the user, your holder application should call the SDK’s
discoverCredentialOffer
function, passing the OpenID4VCI credential offer endpoint’s URI as its uri
parameter. This URI is
retrieved by the application from the scanned QR code.
-
Create a new file named
/app/claim-credential.tsx
and add the following scaffolding code:/app/claim-credential.tsximport { useGlobalSearchParams, useRouter } from "expo-router"; import React, { useCallback, useEffect, useState } from "react"; import { ActivityIndicator, Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native"; import { useHolder } from "@/providers/HolderProvider"; import { type CredentialOfferResponse, discoverCredentialOffer, retrieveCredentials, } from "@mattrglobal/mobile-credential-holder-react-native"; // Claim a Credential - Step 4.1: define the CLIENT_ID and REDIRECT_URI constants export default function ClaimCredential() { const router = useRouter(); const { scannedValue } = useGlobalSearchParams<{ scannedValue: string }>(); const { isHolderInitialised, getMobileCredentials } = useHolder(); const [credentialOffer, setCredentialOffer] = useState<CredentialOfferResponse>(); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const discoverOffer = async () => { if (!isHolderInitialised || !scannedValue) { setError("Missing holder instance or scanned value."); setIsLoading(false); return; } // Claim Credential - Step 3.2: Discover Credential Offer }; discoverOffer(); }, [isHolderInitialised, scannedValue]); const handleConsent = useCallback(async () => { if (!credentialOffer || !isHolderInitialised) { Alert.alert("Error", "Missing offer information or holder instance."); router.back(); return; } // Claim a Credential - Step 4.2: Retrieve Credentials }, [credentialOffer, isHolderInitialised, getMobileCredentials, router]); if (isLoading) { return ( <View style={styles.centered}> <ActivityIndicator size="large" color="#007AFF" /> <Text style={styles.text}>Discovering offer...</Text> </View> ); } if (error || !credentialOffer) { return ( <View style={styles.centered}> <Text style={[styles.text, styles.errorText]}>Error: {error || "No discovery offer found."}</Text> <TouchableOpacity style={styles.button} onPress={() => router.back()}> <Text style={styles.buttonText}>Go Back</Text> </TouchableOpacity> </View> ); } // Claim a Credential - Step 3.3: Display the offer details to the user } const styles = StyleSheet.create({ container: { flex: 1, padding: 16, backgroundColor: "#fff", }, centered: { flex: 1, alignItems: "center", justifyContent: "center", }, title: { fontSize: 22, fontWeight: "600", marginBottom: 10, color: "#000", }, text: { fontSize: 16, color: "#333", marginBottom: 10, }, card: { marginBottom: 16, padding: 10, backgroundColor: "#F5F5F5", borderRadius: 8, borderWidth: 1, borderColor: "#E0E0E0", }, cardTitle: { fontWeight: "700", fontSize: 18, marginBottom: 5, color: "#000", }, button: { backgroundColor: "#007AFF", paddingVertical: 12, paddingHorizontal: 20, borderRadius: 8, alignItems: "center", }, buttonText: { color: "white", fontSize: 16, fontWeight: "600", }, buttonContainer: { marginBottom: 20, gap: 10, }, errorText: { color: "#FF3B30", textAlign: "center", }, });
-
Add the following code under the
// Claim a Credential - Step 3.2: Discover Credential Offer
comment to call the SDK’sdiscoverCredentialOffer
function and discover the credential offer:/app/claim-credential.tsxconst discoveryResult = await discoverCredentialOffer(scannedValue); if (discoveryResult.isErr()) { setError(`Error discovering credential offer: ${discoveryResult.error}`); } else { console.log("Discovered Credential Offer:", discoveryResult.value); setCredentialOffer(discoveryResult.value); } setIsLoading(false);
Now, once the user scans a QR Code that includes a credential offer (e.g. it is prefixed with
openid-credential-offer://
), the SDK’sdiscoverCredentialOffer
function is called and accepts the returnedscannedValue
string (Offer URI) as itsuri
parameter.This is a URL-encoded Credential offer which in our example was embedded in a QR code. In other implementations you might have to retrieve this parameter from a deep-link.
The
discoverCredentialOffer
function makes a request to theuri
URL to retrieve the offer information and returns it as anCredentialOfferResponse
object:CredentialOfferResponse: { _retrieveCredentials: ((options) => Promise<NativeRetrieveCredentialsResponse>); authorizeEndpoint: string; credentialEndpoint: string; credentials: OfferedCredential[]; issuer: string; mdocIacasUri?: string; tokenEndpoint: string; }
Your application can now use the
issuer
(who is offering the credentials) andcredentials
(what credentials are offered and what claims they contain) properties and present this information for the user to review. -
Add the following code under the
// Claim a Credential - Step 3.3: Display the offer details to the user
comment to display the offer details to the user:/app/claim-credential.tsxreturn ( <View style={styles.container}> <Text style={styles.title}>Credential Offer</Text> <Text style={styles.text}> Received {credentialOffer.credentials.length} credential {credentialOffer.credentials.length > 1 ? "s" : ""} from {credentialOffer.issuer} </Text> <ScrollView style={{ flex: 1 }}> {credentialOffer.credentials.map((cred, index) => ( <View key={index} style={styles.card}> <Text style={styles.cardTitle}>{cred.name}</Text> <Text style={styles.text}>Document Type: {cred.doctype}</Text> <Text style={styles.text}>Number of Claims: {cred.claims?.length}</Text> </View> ))} </ScrollView> {/* Claim a Credential - Step 4.3: Add the Consent and Retrieve button */} </View> );
-
Run the app, select the Claim Credential button and scan the following QR code:

You should see a result similar to the following:
As the user scans the QR code, the holder application displays the Credential offer details.
Obtain user consent and initiate credential issuance
The next (and final!) step is to build the capability for the user to accept the credential offer based on the displayed information. This should then trigger issuing the credential and storing it in the holder application.
We will implement the Consent and Retrieve button. Once the user provides their consent by
selecting this button, your application must call the
retrieveCredentials
function to trigger the credential issuance.
-
Add the following constant variables to represent the Authentication provider we will use for this tutorial under the
// Claim a Credential - Step 4.1: define the CLIENT_ID and REDIRECT_URI constants
comment:/app/claim-credential.tsxconst CLIENT_ID = 'react-native-mobile-credential-holder-tutorial-app' const REDIRECT_URI = 'io.mattrlabs.sample.reactnativemobilecredentialholdertutorialapp://credentials/callback'
CLIENT_ID
: This is the identifier that is used by the issuer to recognize the holder application application. This is only used internally in the interaction between the holder application and the issuer and can be any string as long as it is registered with the issuer as a trusted holder application.REDIRECT_URI
: This is the path the user is redirected to after completing authentication with the issuer. Our best practice recommendation is to configure this to be{redirect.scheme}://credentials/callback
as shown in the example above. However, it can be any path as long as it is handled by your application and registered with the issuer against the correspondingCLIENT_ID
.
Both of these parameters must be registered as a key pair as part of the issuer’s OID4VCI workflow configuration. For this tutorial you will be claiming a credential from a MATTR Labs issuer which is pre-configured with the parameters detailed above. We will help you configure your unique values as you move your implementation into production.
-
Add the following code under the
// Claim a Credential - 4.2: Retrieve Credentials
comment to call the SDK’sretrieveCredentials
function to retrieve the offered credentials:/app/claim-credential.tsxconst retrieved = await retrieveCredentials({ autoTrustMobileIaca: true, credentialOffer: credentialOffer, clientId: CLIENT_ID, redirectUri: REDIRECT_URI }) if (retrieved.isErr()) { setError(retrieved.error.message || 'An unexpected error occurred during offer discovery.') Alert.alert('Error', retrieved.error.message || 'Failed to retrieve credentials.') } else { Alert.alert('Success', 'Credentials retrieved successfully!') } // Refresh the list of mobile credentials in the holder application await getMobileCredentials() router.replace('/')
Let’s review where all the parameters come from:
credentialOffer
: This is theCredentialOfferResponse
object returned by thediscoverCredentialOffer
function. It is used by the SDK to retrieve the different issuer endpoints used during the issuance workflow.CLIENT_ID
: Defined in theclaim-credential.tsx
file. It is used by the issuer to identify the holder application that is making a request to claim credentials.REDIRECT_URI
: Defined in theclaim-credential.tsx
file. It is used by the SDK to redirect the user back to a specific holder application screen after completing authentication.autoTrustMobileCredentialIaca
: Set totrue
so that the issuer’s IACA is saved and trusted by default. When set tofalse
, the IACA must be added in advance for the application to trust the issuer and enable claiming the credential.
-
Add the following code under the
// Claim a Credential - Step 4.3: Add the Consent and Retrieve button
comment to display a Consent and Retrieve button which call the applications’handleConsent
function:/app/claim-credential.tsx<View style={styles.buttonContainer}> <TouchableOpacity style={styles.button} onPress={handleConsent}> <Text style={styles.buttonText}>Consent and Retrieve</Text> </TouchableOpacity> <TouchableOpacity style={[styles.button, { backgroundColor: "#FF3B30" }]} onPress={() => router.replace("/")}> <Text style={styles.buttonText}>Cancel</Text> </TouchableOpacity> </View>
Once the handleConsent()
function is called, it will execute the retrieveCredentials
function.
The user will be redirected to
authenticate
with the configured Authentication provider defined in the authorizeEndpoint
element of the
CredentialOfferResponse
object.
Upon successful authentication, the user will proceed to complete the
OID4VCI workflow configured by the issuer. This workflow can include
different steps based on the issuer’s configuration, but eventually the user is redirected to the
configured REDIRECT_URI
which should be handled by your application.
In the example Credential offer used in this tutorial, the issuance workflow immediately proceeds to claiming the credential. Check out our other guides for creating more rich and flexible user experiences.
As the user is redirected to REDIRECT_URI
, the issuer sends the issued mDocs to your application.
The SDK then processes the received credentials and validates them against the
ISO/IEC 18013-5:2021 standard. Credentials who meet
validation rules are stored in the application.
Test the application
- Run the app.
- Select the Scan Credential Offer button.
- Scan the following QR code:

- Select Consent and retrieve Credential(s).
As the user scans the QR code, the holder application retrieves and displays the offer details. The user then provides consent to retrieving the credentials, and the holder application responds by initiating the issuance workflow and displaying the retrieved credentials to the user.
You should see a result similar to the following:
This tutorial uses a demo MATTR Labs Credential offer to issue the credential. This offer uses a workflow that doesn’t actually authenticate the user before issuing a credential, but redirects them to select the credential they wish to issue. In production implementations this must be replaced by a proper authentication mechanism to comply with the ISO/IEC 18013-5:2021 standard and the OID4VCI specification.
Congratulations! Your application can now interact with an OID4VCI Credential offer to claim mDocs!
Summary
You have just used the React Native Holder SDK to build iOS and Android applications that can claim an mDoc issued via an OID4VCI workflow:
This was achieved by building the following capabilities into the application:
- Initialize the SDK so the application can use its functions and classes.
- Interact with a Credential offer formatted as a QR code.
- Retrieve the offer details and present them to the user.
- Obtain user consent and initiate the credential issuance workflow.
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.
- Present a claimed mDoc for verification via a proximity presentation workflow.
- You can check out the React Native Mobile Credential Holder SDK reference documentation to learn more about available functions and classes.