GuidesReact Native Holder SDK🎓 Claim a credential

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:

OID4VCI Tutorial Workflow

  1. The user launches the application and scans a QR code received from an issuer.
  2. The application displays what credential is being offered to the user and by what issuer.
  3. The user agrees to claiming the offered credential.
  4. The user is redirected to complete authentication.
  5. 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:

  • 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

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

  1. Update bundle identifier: Open the app.config.ts file and update the bundleIdentifier value under the // Update the bundle identifier comment to a unique value for your application, e.g. com.mycompany.myapp.

    app.config.ts
    bundleIdentifier: "com.mycompany.myapp",

iOS requires each app to have a unique bundle identifier for App Store and development environments.

  1. Add required app permissions: Face ID (NSFaceIDUsageDescription) and camera usage (NSCameraUsageDescription) permissions are required for different workflows. Add the following permissions to the ios.infoPlist object under the // Add Face ID and Camera usage permissions comment:

    app.config.ts
    NSFaceIDUsageDescription: "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:

app.config.ts
"./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:

BASH
yarn install

Generate the iOS and Android project files

Run the following command to generate the iOS and Android project files:

BASH
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

BASH
yarn ios --device
💡
If you see an error on the first build, select reload.js to resolve it.

Android

BASH
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:

  1. Initialize the SDK.
  2. Interact with a credential offer.
  3. Retrieve offer details and present them to the user.
  4. 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.

  1. In your project’s providers directory, create a new file named WalletProvider.tsx and add the following scaffolding code:

    /providers/WalletProvider.tsx
    import 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.

  1. Add the following code after the // Claim a Credential - Step 1.2: Initialize the Wallet comment to initialize the SDK:

    /providers/WalletProvider.tsx
    const 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 the MobileCredentialHolder extension to enable mDocs capabilities.

  2. Open the /app/_layout.tsx file and replace its content with the following code to wrap the application with the WalletProvider context:

    _layout.tsx
    import { 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

Interact with 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.

  1. 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",
        },
    });
  2. In your project’s components directory, create a new file named QRCodeScanner.tsx and add the following code:

/components/QRCodeScanner.tsx
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,
  1. Next, import the QRCodeScanner component in the index.tsx file by adding the following code under the // Claim a Credential - Step 2.3: Import the QRCodeScanner component comment:

    /app/index.tsx
    import 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://.

  1. 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.tsx
    const 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 the scannedValue 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.

  1. Add the following code under the {/* Claim a Credential - Step 2.5: Add the QRCodeScanner component */} comment to combine the QRCodeScanner component with the handleScanComplete function:
/app/index.tsx
<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.

  1. 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

Present offer details

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.

  1. Create a new file named /app/claim-credential.tsx and add the following scaffolding code:

    /app/claim-credential.tsx
    import { 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",
        },
    });
     
  2. Add the following code under the // Claim a Credential - Step 3.2: Discover Credential Offer comment to call the SDK’s wallet.openid.issuance.discover function and discover the credential offer:

    /app/claim-credential.tsx
    try {
        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’s wallet.openid.issuance.discover function is called and accepts the returned scannedValue string (Offer URI) as its uri 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 the uri URL to retrieve the offer information and returns it as an OpenidIssuanceCredentialOffer 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) and credentials (what credentials are offered and what claims they contain) properties and present this information for the user to review.

  3. 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.tsx
    return (
        <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>
    );
  4. Run the app, select the Claim Credential button and scan the following QR code:

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.

Present offer details

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 via WebBrowser.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.
  1. 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.tsx
    const 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 corresponding CLIENT_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.

  1. Add the following code under the // Claim a Credential - 4.2: Generate Authorization URL comment to call the SDK’s wallet.openid.issuance.generateAuthorizeUrl function and generate an authorization URL:

    /app/claim-credential.tsx
    const 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 the OpenidIssuanceCredentialOffer object returned by the wallet.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 the claim-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 the claim-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 a url variable which represents the authorization URL and is used in the next step.

  2. Add the following code under the // Claim a Credential - 4.3: Open Authentication Session comment to call the SDK’s WebBrowser.openAuthSessionAsync to open an authentication session in a web browser window:

    /app/claim-credential.tsx
    const 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 the wallet.openid.issuance.generateAuthorizeUrl function in the previous step.
    • REDIRECT_URI : Defined in the claim-credential.tsx file.

    The WebBrowser.openAuthSessionAsync function returns an authCode variable which represents the code returned by the authentication server and is used in the next step.

  3. Add the following code under the // Claim a Credential - Step 4.4: Retrieve Token with Authorization Code comment to call the wallet.openid.issuance.retrieveToken function and exchange the authCode authentication code for an access token:

    claim-credential.tsx
    const 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 : The OpenidIssuanceCredentialOffer object returned by the wallet.openid.issuance.discover function in step 3.2 above.
    • codeVerifier : Returned by the wallet.openid.issuance.generateAuthorizeUrl function in the previous step.
    • authCode : Returned by the WebBrowser.openAuthSessionAsync function in the previous step.
    • CLIENT_ID : Defined in the claim-credential.tsx file.
    • REDIRECT_URI : Defined in the claim-credential.tsx file.

    The wallet.openid.issuance.retrieveToken function returns a accessToken variable which represents the access token returned by the issuer and can be used to retrieve the offered credentials.

  4. Add the following code under the // Claim a Credential - Step 4.5: Retrieve and Store Credential in Wallet comment to call the SDK’s wallet.openid.issuance.retrieveCredentials function which uses the returned accessToken to retrieve the offered credential and store it:

    claim-credential.tsx
    const 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 : The OpenidIssuanceCredentialOffer object returned by the wallet.openid.issuance.discover function in step 3.2 above.
    • accessToken : Access token used to access the issuer’s issuance endpoint. Returned by the wallet.openid.issuance.retrieveToken function in the previous step.
    • CLIENT_ID : Defined in the claim-credential.tsx file.
    • autoTrustMobileCredentialIaca : Set to true so that the issuer’s IACA is saved and trusted by default. When set to false, the IACA must be added in advance for the application to trust the issuer and enable claiming the credential.
  5. 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>

Authentication

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

  1. Run the app.
  2. Select the Scan Credential Offer button.
  3. Scan the following QR code:
QR Code
  1. 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.

Credential claimed

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:

OID4VCI Tutorial Workflow

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

  1. Initialize the SDK so the application can use its functions and classes.
  2. Interact with a Credential offer formatted as a QR code.
  3. Retrieve the offer details and present them to the user.
  4. Obtain user consent and initiate the credential issuance workflow.

What’s next?