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 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.
- Download the ZIP file containing the tutorial codebase starting point.
This tutorial is intended for use with the latest versions of MATTR’s React Native Holder SDK and the React Native Mobile Credential Holder SDK extension.
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. Due to MATTR SDK dependency requirements, the tutorial is using React Native v0.73 and Expo SDK v50.
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.
Unzip the tutorial codebase
Unzip the ReactNativeMobileCredentialHolderTutorial.zip
file and open the project in your code
editor.
iOS Application configuration
-
Update bundle identifier: 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 required app permissions: Face ID (
NSFaceIDUsageDescription
) and camera usage (NSCameraUsageDescription
) permissions are required for different workflows. Add the following 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.",
Configure the SDK plugins
The SDK requires platform-specific configurations to work correctly. Three plugin files have already been created in your project root directory. Follow these steps to use these scripts to add the necessary imports to your app configuration.
Add the following plugin imports under the // Configure the SDK plugins
comment in the
app.config.ts
file:
"./withRemoveAPNSCapability.js",
"./withAndroidHolderSDK.js",
"./withExcludedArchitecturesConfig.js",
You can also follow the instructions in the iOS Holder, Android Holder and Android 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 namedWalletProvider.tsx
and add the following scaffolding code:/providers/WalletProvider.tsximport MobileCredentialHolder, { type MobileCredentialMetadata, } from "@mattrglobal/mobile-credential-holder-react-native"; import { type Wallet, initialise } from "@mattrglobal/wallet-sdk-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 WalletContextProps = { wallet: Wallet | null; getMobileCredentials: () => Promise<void>; mobileCredentials: MobileCredentialMetadata[]; deleteMobileCredential: (credentialId: string) => Promise<void>; error: string | null; isLoading: boolean; }; const WalletContext = createContext<WalletContextProps | undefined>(undefined); export function WalletProvider({ children }: { children: React.ReactNode }) { const [wallet, setWallet] = useState<Wallet | null>(null); const [mobileCredentials, setMobileCredentials] = useState<MobileCredentialMetadata[]>([]); const [error, setError] = useState<string | null>(null); const [isLoading, setIsLoading] = useState<boolean>(true); // Online Presentation - Step 3.2: Initialize router variable // Claim a Credential - Step 1.2: Initialize the Wallet const getMobileCredentials = useCallback(async () => { if (!wallet) return; const credentials = await wallet.credential.mobile.getCredentials(); setMobileCredentials(credentials); }, [wallet]); // When the wallet is initialized, get the mobile credentials to display in the app useEffect(() => { if (wallet) { getMobileCredentials(); } }, [wallet, getMobileCredentials]); // An example implementation of deleting a credential, used for demonstration purposes const deleteMobileCredential = useCallback( async (credentialId: string) => { if (!wallet) 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 wallet.credential.mobile.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."); } }, }, ]); }, [wallet, getMobileCredentials] ); // Online Presentation - Step 3.3: Handle deep link const contextValue = useMemo( () => ({ wallet, error, isLoading, getMobileCredentials, mobileCredentials, deleteMobileCredential, }), [wallet, error, isLoading, getMobileCredentials, mobileCredentials, deleteMobileCredential] ); return <WalletContext.Provider value={contextValue}>{children}</WalletContext.Provider>; } export function useWallet() { const context = useContext(WalletContext); if (context === undefined) { throw new Error("useWallet must be used within a WalletProvider"); } 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 Wallet
) to easily locate it in the code.
-
Add the following code after the
// Claim a Credential - Step 1.2: Initialize the Wallet
comment to initialize the SDK:/providers/WalletProvider.tsxconst initialiseWallet = useCallback(async () => { try { const result = await initialise({ extensions: [MobileCredentialHolder], }); if (result.isErr()) { throw new Error(result.error.message || "Failed to initialize wallet."); } setWallet(result.value); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setIsLoading(false); } }, []); useEffect(() => { initialiseWallet(); }, [initialiseWallet]);
This function calls the SDK’s
initialise
method and passes theMobileCredentialHolder
extension to enable mDocs capabilities. -
Open the
/app/_layout.tsx
file and replace its content with the following code to wrap the application with theWalletProvider
context:_layout.tsximport { WalletProvider } from "@/providers/WalletProvider"; import { Stack } from "expo-router"; export default function RootLayout() { return ( // Claim a Credential - Step 1.3: Wrap the app in the WalletProvider component to make the WalletContext available to any child components <WalletProvider> <Stack> <Stack.Screen name="index" options={{ headerTitle: "Home", }} /> </Stack> </WalletProvider> ); }
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 { useWallet } from "@/providers/WalletProvider"; export default function Index() { const router = useRouter(); const { wallet, error, isLoading, mobileCredentials, deleteMobileCredential } = useWallet(); 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 (!wallet) { return ( <View style={styles.centered}> <Text>No wallet 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.replace({ 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 wallet 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 wallet application should call the SDK’s
wallet.openid.issuance.discover
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 * as WebBrowser from "expo-web-browser"; import React, { useCallback, useEffect, useState } from "react"; import { ActivityIndicator, Alert, ScrollView, StyleSheet, Text, TouchableOpacity, View } from "react-native"; import { useWallet } from "@/providers/WalletProvider"; import { type CredentialOffered, CredentialProfileSupported, type MobileCredentialOffered, type OpenidIssuanceCredentialOffer, } from "@mattrglobal/wallet-sdk-react-native"; const isMobileCredentialOffered = (cred: CredentialOffered): cred is MobileCredentialOffered => { return (cred as MobileCredentialOffered).profile === CredentialProfileSupported.Mobile; }; // 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 { wallet, getMobileCredentials } = useWallet(); const [credentialOffer, setCredentialOffer] = useState<OpenidIssuanceCredentialOffer | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const discoverOffer = async () => { if (!wallet || !scannedValue) { setError("Missing wallet instance or scanned value."); setIsLoading(false); return; } // Claim a Credential - Step 3.2: Discover Credential Offer }; discoverOffer(); }, [wallet, scannedValue]); const handleConsent = useCallback(async () => { if (!credentialOffer || !wallet) { Alert.alert("Error", "Missing offer information or wallet instance."); router.back(); return; } try { // Claim a Credential - 4.2: Generate Authorization URL // Claim a Credential - 4.3: Open Authentication Session // Claim a Credential - Step 4.4: Retrieve Token with Authorization Code // Claim a Credential - Step 4.5: Retrieve and Store Credential in Wallet Alert.alert("Success", "Credential added to your wallet."); router.replace("/"); } catch (err: any) { console.error("Error during authorization process:", err); Alert.alert("Error", err.message || "An unexpected error occurred."); } }, [credentialOffer, wallet, 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’swallet.openid.issuance.discover
function and discover the credential offer:/app/claim-credential.tsxtry { const discoveryResult = await wallet.openid.issuance.discover(scannedValue) if (discoveryResult.isErr()) { throw new Error(`Error discovering credential offer: ${discoveryResult.error}`) } setCredentialOffer(discoveryResult.value.offer) } catch (err: any) { console.error('Error during offer discovery:', err) setError(err.message || 'An unexpected error occurred during offer discovery.') } finally { 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’swallet.openid.issuance.discover
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
wallet.openid.issuance.discover()
function makes a request to theuri
URL to retrieve the offer information and returns it as anOpenidIssuanceCredentialOffer
object:OpenidIssuanceCredentialOffer: { authorizeEndpoint: string; authorizeRequestParameters?: AuthorizeRequestParameters; credentialEndpoint: string; credentials: CredentialOffered[]; 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.filter(isMobileCredentialOffered).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.6: 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 wallet 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 wallet.
In our example this is achieved by selecting the Consent and Retrieve button. Once the user
provides their consent by selecting this button, your application must call the
wallet.openid.issuance.retrieveCredentials
function to trigger the credential issuance.
In the next steps we will implement the following steps for claiming a credential:
- Call
wallet.openid.issuance.generateAuthorizeUrl()
to generate an authorization URL, which is opened viaWebBrowser.openAuthSessionAsync()
for user authentication. - Upon successful authentication, the issuer returns an authorization code, which is immediately
exchanged for an access token using
wallet.openid.issuance.retrieveToken()
. - Finally,
wallet.openid.issuance.retrieveCredentials()
uses the returned access token to retrieve and store the credential.
-
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 wallet application. This is only used internally in the interaction between the wallet and the issuer and can be any string as long as it is registered with the issuer as a trusted wallet 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: Generate Authorization URL
comment to call the SDK’swallet.openid.issuance.generateAuthorizeUrl
function and generate an authorization URL:/app/claim-credential.tsxconst generateAuthorizeUrlResult = await wallet.openid.issuance.generateAuthorizeUrl({ offer: credentialOffer, clientId: CLIENT_ID, redirectUri: REDIRECT_URI }) if (generateAuthorizeUrlResult.isErr()) { throw new Error('Error generating authorization URL.') } const { url, codeVerifier } = generateAuthorizeUrlResult.value
Let’s review where all the parameters come from:
credentialOffer
: This is theOpenidIssuanceCredentialOffer
object returned by thewallet.openid.issuance.discover
function in step 3.2 above. 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 wallet 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 wallet application screen after completing authentication.
The
wallet.openid.issuance.generateAuthorizeUrl
function returns aurl
variable which represents the authorization URL and is used in the next step. -
Add the following code under the
// Claim a Credential - 4.3: Open Authentication Session
comment to call the SDK’sWebBrowser.openAuthSessionAsync
to open an authentication session in a web browser window:/app/claim-credential.tsxconst result = await WebBrowser.openAuthSessionAsync(url, REDIRECT_URI, { preferEphemeralSession: false }) if (result.type !== 'success') { throw new Error('Auth session was cancelled or dismissed.') } // Extract authorization code from the redirected URL const codeMatch = result.url.match(/code=([^&]+)/) if (!codeMatch || codeMatch.length < 2) { throw new Error('Authorization code not found in the URL.') } const authCode = codeMatch[1]
url
: Returned by thewallet.openid.issuance.generateAuthorizeUrl
function in the previous step.REDIRECT_URI
: Defined in theclaim-credential.tsx
file.
The
WebBrowser.openAuthSessionAsync
function returns anauthCode
variable which represents the code returned by the authentication server and is used in the next step. -
Add the following code under the
// Claim a Credential - Step 4.4: Retrieve Token with Authorization Code
comment to call thewallet.openid.issuance.retrieveToken
function and exchange theauthCode
authentication code for an access token:claim-credential.tsxconst retrieveTokenResult = await wallet.openid.issuance.retrieveToken({ offer: credentialOffer, codeVerifier, code: authCode, clientId: CLIENT_ID, redirectUri: REDIRECT_URI }) if (retrieveTokenResult.isErr()) { throw new Error(`Error retrieving credential token: ${retrieveTokenResult.error}`) } const { accessToken } = retrieveTokenResult.value
credentialOffer
: TheOpenidIssuanceCredentialOffer
object returned by thewallet.openid.issuance.discover
function in step 3.2 above.codeVerifier
: Returned by thewallet.openid.issuance.generateAuthorizeUrl
function in the previous step.authCode
: Returned by theWebBrowser.openAuthSessionAsync
function in the previous step.CLIENT_ID
: Defined in theclaim-credential.tsx
file.REDIRECT_URI
: Defined in theclaim-credential.tsx
file.
The
wallet.openid.issuance.retrieveToken
function returns aaccessToken
variable which represents the access token returned by the issuer and can be used to retrieve the offered credentials. -
Add the following code under the
// Claim a Credential - Step 4.5: Retrieve and Store Credential in Wallet
comment to call the SDK’swallet.openid.issuance.retrieveCredentials
function which uses the returnedaccessToken
to retrieve the offered credential and store it:claim-credential.tsxconst retrieveCredentialsResult = await wallet.openid.issuance.retrieveCredentials({ offer: credentialOffer, accessToken, clientId: CLIENT_ID, autoTrustMobileCredentialIaca: true // Automatically trust the issuer }) // An example of how to handle the retrieved credentials retrieveCredentialsResult.forEach((credential) => { if (!credential) return if ('result' in credential) { const result = credential.result console.log(result) } else { console.error(credential.error) } }) // Refresh the list of mobile credentials in the wallet await getMobileCredentials()
credentialOffer
: TheOpenidIssuanceCredentialOffer
object returned by thewallet.openid.issuance.discover
function in step 3.2 above.accessToken
: Access token used to access the issuer’s issuance endpoint. Returned by thewallet.openid.issuance.retrieveToken
function in the previous step.CLIENT_ID
: Defined in theclaim-credential.tsx
file.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.6: 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.back()}> <Text style={styles.buttonText}>Cancel</Text> </TouchableOpacity> </View>
Once the handleConsent()
function is called, it will execute all of the functions calls
implemented in steps 4.2.-4.5 above. The user will be redirected to
authenticate
with the configured Authentication provider defined in the authorizeEndpoint
element of the
DiscoveredCredentialOffer
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 wallet retrieves and displays the offer details. The user then provides consent to retrieving the credentials, and the wallet 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 Holder SDK and the React Native Mobile Credential Holder SDK extension reference documentation to learn more about available functions and classes.