light-mode-image
Learn

Learn how to build an application that can claim an mDoc via OID4VCI

Coming soon...

Introduction

In this tutorial, you will learn how to use the mDocs Holder SDKs to build an application that can claim an mDoc issued via both the OID4VCI Authorization Code and Pre-authorized Code flows.

OID4VCI Authorization Code 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 (Only in the Authorization Code flow).
  5. Upon successful authentication, the credential is issued to the user's application, where they can now store, view and present it.

The result will look something like this:

Prerequisites

Before you get 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 resources for more information:

  • We assume you have experience developing applications in the relevant programming languages and frameworks (Swift for iOS, Kotlin for Android and TypeScript for React Native).

If you need to get a holding solution up and running quickly with minimal development resources and in-house domain expertise, talk to us about our white-label MATTR GO Hold app which might be a good fit for you.

Assets

  • As part of your onboarding process you should have been provided with access to the following assets (Contact us if you are interested in trialing the SDK):
    • ZIP file which includes the required framework: (MobileCredentialHolderSDK-*version*.xcframework.zip).
    • Sample Wallet app: You can use this app for reference as you work through this tutorial.

This tutorial is only meant to be used with the most recent version of the iOS mDocs Holder SDK.

  • As part of your onboarding process you should have been provided with access to the following assets (Contact us if you are interested in trialing the SDK):
    • ZIP files that include the required library (common-*version*.zip, holder-*version*.zip).
    • Sample Wallet app: You can use this app for reference as you work through this tutorial.

This tutorial is only meant to be used with the most recent version of the Android mDocs Holder SDK.

  • You will need access to the SDK and additional MATTR dependencies to complete this tutorial. Contact us if you are interested in trialing the SDK.

This tutorial is intended for use with the latest version of MATTR's React Native mDocs Holder SDK.

Development environment

  • Xcode setup with either:
    • Local build settings if you are developing locally.
    • iOS developer account if you intend to publish your app.

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 device to run the built application on, setup with:
    • Biometric authentication (Face ID, Touch ID).
    • Available internet connection.
  • Supported Android device to run the built application on, setup with:
    • Biometric authentication (Face recognition, fingerprint recognition).
    • Available internet connection.
    • Debugging enabled.
  • Supported iOS and/or Android device to run the built application on, setup with:
    • Available internet connection.
    • iOS:
      • Biometric authentication (Face ID, Touch ID).
    • Android:
      • Biometric authentication (Face recognition, fingerprint recognition).
      • Debugging enabled.

Got everything? Let's get going!

Environment setup

Perform the following steps to setup and configure your development environment:

Step 1: Create a new project

Please follow the detailed instructions to Create a new Xcode project and add your organization’s identifier.

Create a new project

Step 2: Unzip the dependencies file

  1. Unzip the MobileCredentialHolderSDK-*version*.xcframework.zip file.
  2. Drag the MobileCredentialHolderSDK-*version*.xcframework folder into your project.
  3. Configure MobileCredentialHolderSDK.xcframework to Embed & sign.

See Add existing files and folders for detailed instructions.

This should result in the the following framework being added to your project:

Framework added

Step 3: Configure required resources

  1. Create a new file named Constants.swift within your project.

  2. Add the following string resources to represent the Authentication provider you will use for this tutorial:

    Constants.swift
    enum Constants {
    	static let redirectUri: String = "io.mattrlabs.sample.mobilecredentialholderapp://credentials/callback"
    	static let clientId: String = "ios-sample-mobile-credential-holder-app"
    }
    • redirectUri : This is the path the SDK will redirect to once the user completes 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 clientId.
    • clientId : 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.

Both of these parameters are only used in the Authorization Code flow and must be registered as a key pair as part of the issuer's OID4VCI workflow configuration. In this tutorial you will be claiming a credential from a MATTR Labs issuer which is configured with the parameters detailed above. We will help you configure your unique values as you move your implementation into production.

Step 4: Add Bluetooth and biometric permissions

The SDK requires access to the mobile device Bluetooth and biometric capabilities for the different workflows built in this tutorial. Configure these permissions in the Info tab of the Application target:

Privacy capabilities

Step 5: Run the application

Select Run and make sure the application launches with a “Hello, world!” text in the middle of the display, as shown in the following image:

Application ready

Step 1: Create a new project

  1. Create a new Android Studio project, using the Empty Activity template.

Create a new Android project

  1. Name the project Holder Tutorial.
  2. Select API 24 as the Minimum SDK version.
  3. Select Kotlin DSL as the Build configuration language.

Project configuration

  1. Sync the project with Gradle files.

Step 2: Add required dependencies

  1. Select the Project view.

    Project view

  2. Create a new directory named repo in your project's folder.

  3. Unzip the common-*version*.zip and holder-*version*.zip files and copy the unzipped global folders into the new repo folder, merging the top-level directories.

    Unzipped files copied

  4. Open the settings.gradle.kts file in the HolderTutorial folder and add the following Maven repository to the dependencyResolutionManagement.repositories block:

    settings.gradle.kts
    maven {
        url = uri("repo")
    }
  5. Open the app/build.gradle.kts file in your app folder and add the following dependencies to the dependencies block:

    app/build.gradle.kts
    implementation("global.mattr.mobilecredential:holder:4.1.0")
    implementation("androidx.navigation:navigation-compose:2.9.0")
  • The holder dependency version should match the version of the unzipped holder-*version*.zip file you copied to the repo folder.
  • The required navigation-compose version may differ based on your version of the IDE, Gradle, and other project dependencies.
  1. In your app's AndroidManifest.xml file, add the following activity declaration. This represents the Authentication provider you will use for this tutorial:

    AndroidManifest.xml
    <activity
        android:name="global.mattr.mobilecredential.common.webcallback.WebCallbackActivity"
        tools:ignore="IntentFilterExportedReceiver"
        tools:node="merge">
        <intent-filter>
            <action android:name="android.intent.action.VIEW" />
    
            <category android:name="android.intent.category.DEFAULT" />
            <category android:name="android.intent.category.BROWSABLE" />
    
            <data
                android:host="credentials"
                android:scheme="io.mattrlabs.sample.mobilecredentialtutorialholderapp" />
        </intent-filter>
    </activity>

    These values are part of the redirect URI the SDK will redirect the user to once they complete Authentication with the issuer:

    • host : The host can be any path, but our best practice recommendation is to configure this to be credentials, as the standard format for the redirect URI is {redirect.scheme}://credentials/callback.
    • scheme : The scheme can be any path that is handled by your application and registered with the issuer.

These values (alongside the client ID, which will be discussed later) are only used in the Authorization Code flow and must be registered as part of the issuer's OID4VCI workflow redirect URI. In this tutorial you will be claiming a credential from a MATTR Labs issuer which is already configured with the parameters detailed above. We will help you configure your unique values as you move your implementation into production.

  1. Sync the project with Gradle files.

  2. Open the Build tab and select Sync to make sure that the project has synced successfully.

    Synced successfully

Step 3: Run the application

  1. Connect a debuggable Android mobile device to your machine.

  2. Build and run the app on the connected mobile device.

    The app should launch with a “Hello, Android!” text displayed:

    Blank app

Step 1: Access the tutorial codebase

  1. Access the tutorial starter codebase by either:

    • Cloning the MATTR sample-apps repository:

      Clone the repository
      git 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.

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

Step 2: iOS Application configuration

  1. 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 the following Face ID (NSFaceIDUsageDescription) and camera usage (NSCameraUsageDescription) 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.",

    These permissions are required for the SDK to use biometric authentication and the camera for QR code scanning.

  2. Add the following code under the // Add Bluetooth permissions. comment in the app.config.ts file to add Bluetooth permissions to the application:

    app.config.ts
    NSBluetoothAlwaysUsageDescription: "This app uses Bluetooth to communicate with verifiers or holders.",
    NSBluetoothPeripheralUsageDescription: "This app uses Bluetooth to communicate with verifiers or holders.",

    These permissions will be required for the SDK to use Bluetooth for device engagement and communication in the proximity presentation tutorial.

Step 3: Configure the SDK plugins

Add the following code under the // Configure the SDK plugins comment to import required plugin configurations:

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

Step 4: Install the dependencies

Open a terminal in the project's root and install the application dependencies:

BASH
yarn install

Step 5: 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.

Step 6: Start the application

Connect your testing device(s) and run the following command to start the application(s):

iOS

BASH
yarn ios --device

Android

BASH
yarn android --device

Nice work, your application is now all set to begin using the SDK!

Tutorial steps

In this part of the tutorial you will build the capabilities for the user to interact with an OID4VCI credential offer and claim an mDoc.

To achieve this, you will break this capability down into the following steps:

  1. Initialize the SDK.
  2. Interact with a Credential offer.
  3. Retrieve offer details and present them to the Holder.
  4. Obtain user consent and initiate Credential issuance.

Step 1: Initialize the SDK

The first capability you will build into your app is to initialize the SDK so that your app can use SDK methods and classes. To achieve this, you need to import the MobilecredentialHolderSDK framework and then initialize the MobileCredentialHolder class:

  1. Open the ContentView class in your new app project and replace any existing code with the following:
ContentView
import SwiftUI
// Claim Credential - Step 1.2: Import MobileCredentialHolderSDK


struct ContentView: View {
   @ObservedObject var viewModel: ViewModel = ViewModel()
   var body: some View {
       NavigationStack(path: $viewModel.navigationPath) {
           VStack {
               Button("Scan Credential Offer") {
                   viewModel.navigationPath.append(NavigationState.qrScan)

               }
               .padding()
               createQRCodeButton
               if viewModel.shouldDisplayOnlinePresentation {
                   Button("View Online Presentation Session") {
                       viewModel.navigationPath.append(NavigationState.onlinePresentation)
                   }
                   .padding()

               }
               Spacer()
           }
           .padding()
           .navigationDestination(for: NavigationState.self) { destination in
               switch destination {
               case .qrScan:
                   codeScannerView
               case .credentialOffer:
                   credentialOfferView
               case .transactionCodeInput:
                   transactionCodeInputView
               case .retrievedCredentials:
                   retrievedCredentialsView
               case .onlinePresentation:
                   // Online Presentation - Step 3.3: Display online presentation view
                   EmptyView()
               case .presentCredentials:
                   qrCodeView
               case .proximityPresentation:
                   // Proximity Presentation - Step 2.5: Display proximity presentation view
                   EmptyView()
               }
           }
           // Online Presentation - Step 2.4: Create session from request URI
       }
   }

   // MARK: - Credential Retrieval Views

   var codeScannerView: some View {
       // Claim Credential - Step 2.4 Create QRScannerView
       EmptyView()
   }

   var credentialOfferView: some View {
       // Claim Credential - Step 3.5: Display Credential offer
       EmptyView()
   }

   var transactionCodeInputView: some View {
       // Claim Credential - Step: 3.4 Display transaction code input view.
       EmptyView()
   }


   var retrievedCredentialsView: some View {
       // Claim Credential - Step 4.4: Display retrieved credentials
       EmptyView()
   }

   // MARK: - Proximity Presentation Views

   var createQRCodeButton: some View {
       // Proximity Presentation - Step 1.5: Add button to generate QR code
       EmptyView()
   }

   func generateQRCode(data: Data) -> Data? {
       // Proximity Presentation - Step 1.6: Generate QR code
       return nil
   }

   var qrCodeView: some View {
       // Proximity Presentation - Step 1.7: Create QR code view
       EmptyView()
   }
}

class ViewModel: ObservableObject {
   @Published var navigationPath = NavigationPath()
   // Claim Credential - Step 1.3: Add MobileCredentialHolder var

   // Claim Credential - Step 3.1: Add DiscoveredCredentialOffer and discoveredCredentialOfferURL vars

   // Claim Credential - Step 4.1: Add retrievedCredentials var


   // Proximity Presentation - Step 1.2: Create deviceEngagementString and proximityPresentationSession variables


   // Proximity and Online Presentation: Create variables for credential presentations


   // Online Presentation - Step 2.1: Create a variable to hold the online presentation session object


   var shouldDisplayOnlinePresentation: Bool {
       // Online Presentation - Step 3.4: View Online Presentation
       return false
   }

   // Claim Credential - Step 1.4: Initialize MobileCredentialHolder SDK

   @MainActor
   func getCredential(id: String) {
       // Proximity and Online Presentation: Retrieve a credential from storage
       print("This method will get a credential from storage and update the viewModel")
   }
}

// MARK: - Credential Retrieval

extension ViewModel {
   @MainActor
   func discoverCredentialOffer(_ offer: String) {
       // Claim Credential - Step 3.2: Add discover credential offer logic
       print("This method will discover a credentials offer and update viewModel")
   }

   @MainActor
   func retrieveCredential(transactionCode: String?) {
       // Claim Credential - Step 4.2: Call retrieveCredential method
       print("This method will save a credential from offer and store it in the application's storage ")
   }
}

// MARK: - Online Presentation

extension ViewModel {
   @MainActor
   func createOnlinePresentationSession(authorizationRequestURI: String) async {
       // Online Presentation - Step 2.3: Create online presentation session
       print("This method will create an online presentation session and update viewModel")
   }

   @MainActor
   func sendOnlinePresentationSessionResponse(id: String) {
       // Online Presentation - Step 4.1: Send online presentation response
       print("This method will be passed to a view and send a response with selected credentials")
   }
}

// MARK: - Proximity Presentation

extension ViewModel {
   func createDeviceEngagementString() {
       // Proximity Presentation - Step 1.3: Create function to create a proximity presentation session and generate QR code
       print("This method will create a device engagement string that will be converted to a QR code")
   }

   // Proximity Presentation - Step 1.4: Update function signature
   func onRequestReceived() {
       // Proximity Presentation - Step 2.2: Store credential requests and matched credentials
       print("The signature of this method will need to be updated to include the correct parameters")
       print("This is a method that will be called when a proximity presentation request is received")
   }

   @MainActor
   func sendProximityPresentationResponse(id: String) {
       // Proximity Presentation - Step 3.1: Send a credential response
       print("This method will be passed to a view and send a response with selected credentials")

   }
}

// MARK: - Navigation

enum NavigationState: Hashable {
   case qrScan
   case credentialOffer
   case transactionCodeInput
   case retrievedCredentials
   case onlinePresentation
   case presentCredentials
   case proximityPresentation
}

This will serve as the basic structure for your application for this and the future tutorials. We will copy and paste different code snippets into specific locations to achieve the different functionalities. These locations are indicated by comments that reference both the section and the step (e.g. // Claim Credential - Step 1.2: Import MobileCredentialHolderSDK).

We recommend copying and pasting the comment text to easily locate it in the code.

  1. Add the following code after the // Claim Credential - Step 1.2: Import MobileCredentialHolderSDK comment to import MobileCredentialHolderSDK and enable using its capabilities in your application:

    ContentView
     import MobileCredentialHolderSDK
  2. Add the following code after the // Claim Credential - Step 1.3: Add MobileCredentialHolder var comment to create a variable that holds the mobileCredentialHolder instance:

    ContentView
        var mobileCredentialHolder: MobileCredentialHolder
  3. Add the following code after the // Claim Credential - Step 1.4: Initialize MobileCredentialHolder SDK comment to initialize the SDK and make it available for your application:

    ContentView
    init() {
    do {
        mobileCredentialHolder = MobileCredentialHolder.shared
        try mobileCredentialHolder.initialize(
            userAuthenticationConfiguration: UserAuthenticationConfiguration(userAuthenticationBehavior: .onDeviceKeyAccess),
            credentialIssuanceConfiguration: CredentialIssuanceConfiguration(
                redirectUri: Constants.redirectUri,
                autoTrustMobileCredentialIaca: true
            )
        )
    } catch {
        print(error)
    }
    }

    Let's review the parameters that are passed into initialize:

  • userAuthenticationConfiguration: Defines when is user authentication required. In this example, authentication will only be required when a credential is issued and/or presented. Refer to the SDK Docs to see all options.

  • CredentialIssuanceConfiguration :

    • redirectUri: This is the URI that the SDK uses to redirect the user back to your wallet application after authentication is complete. It must match the value you configured earlier in the development environment setup. This value is only used and required in the Authorization Code flow.
    • autoTrustMobileCredentialIaca: Controls how the SDK handles issuer IACA certificates during credential issuance.
      • If set to true, the SDK will automatically download and trust the issuer’s IACA certificate(s) when claiming a credential. This allows credentials to be claimed from any issuer.
      • If set to false, the SDK will only accept credentials from issuers whose IACA certificates have already been manually added to the SDK’s trusted issuers list (see addTrustedIssuerCertificates). This requires the application to manually manage IACA certificates.
  1. Run the app to make sure it compiles properly.

The first capability you will build into your app is to initialize the SDK so that your app can use SDK functions and classes. To achieve this, you need to initialize the MobileCredentialHolder class:

  1. Open the MainActivity file in your project and replace any existing code with the following:

    MainActivity.kt
    package com.example.holdertutorial
    
    import android.app.Activity
    import android.content.Intent
    import android.os.Bundle
    import android.util.Log
    import android.widget.Toast
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.activity.enableEdgeToEdge
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.Spacer
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.fillMaxWidth
    import androidx.compose.foundation.layout.padding
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.items
    import androidx.compose.material3.Button
    import androidx.compose.material3.Card
    import androidx.compose.material3.OutlinedTextField
    import androidx.compose.material3.Scaffold
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.unit.dp
    import androidx.lifecycle.lifecycleScope
    import androidx.navigation.NavController
    import androidx.navigation.compose.NavHost
    import androidx.navigation.compose.composable
    import androidx.navigation.compose.rememberNavController
    import androidx.navigation.navDeepLink
    import com.example.holdertutorial.ui.theme.HolderTutorialTheme
    import global.mattr.mobilecredential.common.dto.MobileCredential
    import global.mattr.mobilecredential.holder.MobileCredentialHolder
    import global.mattr.mobilecredential.holder.ProximityPresentationSession
    import global.mattr.mobilecredential.holder.issuance.CredentialIssuanceConfiguration
    import global.mattr.mobilecredential.holder.issuance.dto.DiscoveredCredentialOffer
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.launch
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            enableEdgeToEdge()
            // Claim Credential - Step 1.2: Initialize the SDK
    
            setContent {
                HolderTutorialTheme {
                    val navController = rememberNavController()
                    Scaffold { innerPadding ->
                        NavHost(
                            modifier = Modifier
                                .fillMaxSize()
                                .padding(innerPadding)
                                .padding(8.dp),
                            startDestination = "home",
                            navController = navController,
                        ) {
                            composable("home") {
                                HomeScreen(this@MainActivity, navController)
                            }
                            composable("scanOffer") {
                                // Claim Credential - Step 2.5: Add "Scan Offer" screen call
                            }
                            composable("retrievedCredential") {
                                // Claim Credential - Step 4.9: Add "Retrieved Credential" screen call
                            }
                            composable("presentationQr") {
                                // Proximity Presentation - Step 1.2: Add "QR Presentation" screen call
                            }
                            composable("presentationSelectCredentials") {
                                // Proximity Presentation - Step 2.6: Add "Select Credential" screen call
                            }
                            // Online Presentation - Step 2.2: Add "Online Presentation" screen call
                        }
                    }
                }
            }
        }
    }
    
    @Composable
    fun HomeScreen(activity: Activity, navController: NavController) {
        val coroutineScope = rememberCoroutineScope()
        var transactionCode by remember { mutableStateOf("") }
    
        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = { navController.navigate("scanOffer") }, Modifier.fillMaxWidth()) {
                Text("Scan Credential Offer")
            }
    
            // Proximity Presentation - Step 1.3: Add button for starting the credentials presentation workflow
    
            // Claim Credential - Step 3.3: Display discovered credential offer
        }
    }
    
    // Claim Credential - Step 4.4: Create function to retrieve credentials
    
    object SharedData {
        // Claim Credential - Step 3.1: Add discovered credential offer variables
        // Claim Credential - Step 4.2: Add retrieved credentials variable
        // Proximity Presentation - Step 2.1: Add proximity presentation request variable
    }

This will serve as the basic structure for your application. We will copy and paste different code snippets into specific locations in this codebase to achieve the different functionalities. These locations are indicated by comments that reference both the section and the step.

We recommend leaving the comment text (e.g. // Claim Credential - Step 1.2: Initialize the SDK) even after you have pasted the code snippet, as it will later help you to easily locate the step in the code.

  1. Add the following code after the // Claim Credential - Step 1.2: Initialize the SDK comment to initialize the SDK by creating a new instance of the MobileCredentialHolder class:

    MainActivity.kt
    lifecycleScope.launch {
        try {
            MobileCredentialHolder.getInstance().initialize(
                context = this@MainActivity,
                // Step 4.1: Add credential issuance configuration
            )
        } catch (e: Exception) {
            Log.e("MainActivity", "SDK initialization failed", e)
        }
    }

This will initialize the SDK, making it available for your application.

  1. Run the app to make sure it compiles properly.

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 HolderProvider.tsx and add the following scaffolding code:

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

  1. Add the following code under the // Claim a Credential - Step 1.2: Initialize the Holder SDK comment to call the SDK's initialise method and initialize the SDK:

    /providers/HolderProvider.tsx
    const 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]);
  2. Open the /app/_layout.tsx file and replace its content with the following code to wrap the application with the HolderProvider context:

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

Step 2: Interact with a Credential offer

Interact with Credential offer

Users can receive OID4VCI Credential offers as deep-links or QR codes. In this tutorial you will use a MATTR Labs OID4VCI Credential offer rendered as a QR code.

Creating your own Credential offer is not within the scope of the current tutorial. You can follow the OID4VCI guide that will walk you through creating one.

Your application needs to let users interact with Credential offers. Since this tutorial uses a QR code to deliver the offer, your application must be able to scan and process QR codes.

For ease of implementation, you will use a third party framework to achieve this:

  1. Add camera usage permissions to the app target:

Camera permissions

  1. Add the CodeScanner library via Swift Package Manager.

Code scanner package

  1. Create a new swift file named QRScannerView and add the following code into it to implement the QR scanning capability:

    QRScannerView
        import SwiftUI
        import CodeScanner
    
        struct QRScannerView: View {
    
            private let completionHandler: (String) -> Void
    
            init(completion: @escaping (String) -> Void) {
                completionHandler = completion
            }
    
            var body: some View {
                CodeScannerView(codeTypes: [.qr]) { result in
                    switch result {
                    case .failure(let error):
                        print(error.localizedDescription)
                    case .success(let result):
                        print(result.string)
                        completionHandler(result.string)
                    }
                }
            }
    }
  2. Return to the ContentView file and replace the EmptyView() under the // Claim Credential - Step 2.4 Create QRScannerView comment with the following code to create a new QRScannerView view in the application for scanning QR codes:

    ContentView
            QRScannerView(
                completion: { credentialOffer in
                    viewModel.discoverCredentialOffer(credentialOffer)
                }
            )
  3. Run the app and tap the Scan Credential Offer button. When prompted, grant camera access to allow QR code scanning.

You should see a result similar to the following:

As the user selects the Scan Credential Offer button, the app launches the device camera to enable the user to scan a QR code.

You might notice that nothing happens after scanning a QR code - this is expected. In the next step you will implement the logic that retrieves the credential offer details from the QR code and presents them to the user.

For ease of implementation, you will use a third party library to achieve this:

  1. Add the following dependencies to your app/build.gradle.kts file:

    app/build.gradle.kts
    implementation("com.google.accompanist:accompanist-permissions:0.36.0")
    implementation("com.journeyapps:zxing-android-embedded:4.3.0")
  2. Sync your project with Gradle files.

  3. In your package, create a new file named ScanOfferScreen.kt.

    Scan Offer screen created

  4. Add the following code to the new file:

    ScanOfferScreen.kt
    import android.Manifest
    import android.app.Activity
    import android.content.Context
    import android.util.Log
    import android.widget.Toast
    import androidx.activity.compose.rememberLauncherForActivityResult
    import androidx.activity.result.contract.ActivityResultContracts
    import androidx.compose.foundation.layout.Box
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.material3.CircularProgressIndicator
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.DisposableEffect
    import androidx.compose.runtime.LaunchedEffect
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.viewinterop.AndroidView
    import androidx.navigation.NavController
    import com.google.accompanist.permissions.ExperimentalPermissionsApi
    import com.google.accompanist.permissions.isGranted
    import com.google.accompanist.permissions.rememberPermissionState
    import com.journeyapps.barcodescanner.BarcodeCallback
    import com.journeyapps.barcodescanner.DecoratedBarcodeView
    import global.mattr.mobilecredential.holder.MobileCredentialHolder
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.launch
    
    // Gets the permissions and shows the screen content, when the permissions are obtained
    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun ScanOfferScreen(navController: NavController) {
        val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
    
        val requestPermissionLauncher =
            rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {}
    
        LaunchedEffect(cameraPermissionState) {
            if (!cameraPermissionState.status.isGranted) {
                requestPermissionLauncher.launch(Manifest.permission.CAMERA)
            }
        }
    
        if (cameraPermissionState.status.isGranted) Content(navController)
    }
    
    // Screen content
    @Composable
    private fun Content(navController: NavController) {
        val context = LocalContext.current
        val barcodeView = remember { DecoratedBarcodeView(context) }
        val coroutineScope = rememberCoroutineScope()
        var isQrScanned by remember { mutableStateOf(false) }
    
        val barcodeCallback = remember {
            BarcodeCallback { result ->
                // Executed when the QR code was scanned
                coroutineScope.launch { onQrScanned(context, result.text, navController) }
                barcodeView.pause()
                isQrScanned = true
            }
        }
    
        // Setting up the QR scanner
        DisposableEffect(Unit) {
            barcodeView.decodeContinuous(barcodeCallback)
            barcodeView.resume()
            onDispose { barcodeView.pause() }
        }
    
        // Showing the scanner until the QR is scanned. Showing a progress bar after that
        if (!isQrScanned) {
            AndroidView(factory = { barcodeView }, modifier = Modifier.fillMaxSize())
        } else {
            Box(Modifier.fillMaxSize()) {
                CircularProgressIndicator(Modifier.align(Alignment.Center))
            }
        }
    }
    
    private suspend fun onQrScanned(context: Context, offer: String, navController: NavController) {
        // Step 3.2: Discover credential offer
    }
  5. Back in the MainActivity file, add the following code under the // Claim Credential - Step 2.5: Add Scan Offer screen call comment to connect the created composable to the navigation graph:

    MainActivity.kt
    ScanOfferScreen(navController)
  6. Run the app and select the Scan Credential Offer button.

As the user selects the Scan Credential Offer button, the app asks for camera permission, and launches the device camera to enable the user to scan a QR code.

  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 { 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",
      },
    });
  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.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 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:

Step 3: Retrieve offer details and present them to the user

Present offer details

Next, you'll add the ability to display the details of the Credential offer to the user before they decide to claim any credentials. This process, known as credential discovery, allows your wallet application to retrieve and present 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 application should call the SDK's discoverCredentialOffer method. We are going to implement this within the ViewModel class.

  1. Add the following code under the // Claim Credential - Step 3.1: Add DiscoveredCredentialOffer and discoveredCredentialOfferURL vars comment to add new variables that will hold the credential offer details:

    ContentView
    @Published var discoveredCredentialOffer: DiscoveredCredentialOffer?
    var discoveredCredentialOfferURL = ""
  2. Replace the print statement under the // Claim Credential - Step 3.2: Add discover credential offer logic comment with the following code to create a function that calls the SDK's discoverCredentialOffer method:

    ContentView
        Task {
            do {
                discoveredCredentialOffer = try await mobileCredentialHolder.discoverCredentialOffer(offer)
                // save the url to use for credential retrieval
                discoveredCredentialOfferURL = offer
                // present credential offer screen, as soon as credential offer is discovered
                navigationPath.append(NavigationState.credentialOffer)
            } catch {
                print(error)
            }
        }

    This function is called from our QRScannerView callback, so that when the user scans a QR Code that includes a credential offer, the discoverCredentialOffer method is called and accepts the returned credentialOffer string as its offer parameter.

    This is a URL-encoded Credential offer which in our example is embedded in a QR code. In other implementations you might have to retrieve this parameter from a deep-link.

    The discoverCredentialOffer method makes a request to the offer URL to retrieve the offer details and returns it as a DiscoveredCredentialOffer object:

    Swift
    struct DiscoveredCredentialOffer {
        let issuer: URL
        let credentials: [OfferedCredential]
        let transactionCode: TransactionCode?
    }

    The application can now use the issuer and credentials properties and present this information for the user to review. Once an application has discovered a credential offer, the user is navigated to the credentialOfferView view, which you are going to implement next.

    Next you will use the transactionCode property to inspect whether or not the issuer requires a transaction code to claim the credential. This will enable the app to handle offers with and without a transaction code.

  3. Create a new file named transactionCodeInputView and paste the following code to create a view that allows the user to input a transaction code when it is required by the issuer:

    transactionCodeInputView
    import SwiftUI
    
    struct TransactionCodeInputView: View {
        @ObservedObject var viewModel: ViewModel
        @State private var transactionCode = ""
        @Environment(\.dismiss) private var dismiss
    
        var body: some View {
            VStack(spacing: 20) {
                Text("Transaction Code Required")
                    .font(.title2)
                    .fontWeight(.bold)
    
                Text("Please enter the transaction code to proceed with credential retrieval.")
                    .multilineTextAlignment(.center)
                    .foregroundColor(.secondary)
    
                TextField("Enter transaction code", text: $transactionCode)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(.horizontal)
    
                HStack(spacing: 20) {
                    Button("Cancel") {
                        dismiss()
                    }
                    .buttonStyle(.bordered)
    
                    Button("Retrieve Credentials") {
                        viewModel.retrieveCredential(transactionCode: transactionCode)
                    }
                    .buttonStyle(.borderedProminent)
                    .disabled(transactionCode.isEmpty)
                }
    
                Spacer()
            }
            .padding()
            .navigationTitle("Transaction Code")
            .navigationBarBackButtonHidden(false)
        }
    }

    The application will only show this view when the credential offer indicates a transaction code is required. The user will then be able to input a transaction code they had received separately from the issuer.

  4. Return to ContentView file and replace the EmptyView under the // Claim Credential - Step: 3.4 Display transaction code input view comment with the following code to make use of the new view:

    Swift
    TransactionCodeInputView(viewModel: viewModel)
  5. Replace the EmptyView under the // Claim Credential - Step 3.5: Display Credential offer comment with the following code to navigate the user to the credentialOfferView view when a credential offer is discovered:

    ContentView
        VStack {
            Text("Received \(viewModel.discoveredCredentialOffer?.credentials.count ?? 0) Credential Offer(s)")
                .font(.headline)
            Text("from \(viewModel.discoveredCredentialOffer?.issuer.absoluteString ?? "unknown issuer")")
                .font(.subheadline)
            List(viewModel.discoveredCredentialOffer?.credentials ?? [], id: \.docType) { credential in
                Section {
                    HStack {
                        Text("Name:")
                            .bold()
                        Spacer()
                        Text("\(credential.name ?? "")")
                    }
                    HStack {
                        Text("Doctype:")
                            .bold()
                        Spacer()
                        Text("\(credential.docType)")
                    }
                    HStack {
                        Text("No. of claims:")
                            .bold()
                        Spacer()
                        Text("\(credential.claims.count)")
                    }
                }
            }
            Button {
                if viewModel.discoveredCredentialOffer?.transactionCode != nil {
                    viewModel.navigationPath.append(NavigationState.transactionCodeInput)
                    return
                }
                viewModel.retrieveCredential(transactionCode: nil)
            } label: {
                Text("Consent and retrieve Credential(s)")
                    .font(.title3)
            }
            .buttonStyle(.borderedProminent)
            .clipShape(Capsule())
        }

    The app now handles selection of the Consent and retrieve Credential(s) button based on the retrieved offer details:

    • For Pre-authorized Code offers:
      • If a transaction code is required, it will navigate the user to the TransactionCodeInputView view.
      • If no transaction code is required, it will call viewModel.retrieveCredential(transactionCode: nil) to retrieve the credential.
    • For Authorization Code offers:
      • The SDK will automatically redirect the user to a web browser to authenticate with issuer before continuing to retrieve the credential.
  6. Run the app, select the Scan Credential Offer 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 application displays the credential offer details.

You might notice that nothing happens if you select the Consent and retrieve Credential(s) button. This is expected - in the next step you will implement the logic that initiates the credential issuance once the user provides their consent.

To display this information to the user, your application should call the discoverCredentialOffer function.

  1. In your MainActivity file, add the following code under the // Claim Credential - Step 3.1: Add discovered credential offer variables comment to add new variables that will hold the credential offer details:

    MainActivity.kt
    var scannedOffer: String? = null
    var discoveredCredentialOffer: DiscoveredCredentialOffer? = null
  2. In your ScanOfferScreen file, add the following code under the // Step 3.2: Discover credential offer comment to handle the credential offer upon scanning a QR code:

    ScanOfferScreen.kt
    try {
        SharedData.scannedOffer = offer
        SharedData.discoveredCredentialOffer =
            MobileCredentialHolder.getInstance().discoverCredentialOffer(offer)
    } catch (e: Exception) {
        Toast.makeText(context, "Failed to discover offer", Toast.LENGTH_SHORT).show()
    }
    
    navController.navigateUp()

    Now, once the user scans a QR Code, the discoverCredentialOffer function is called and accepts the returned offer string as its 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 the offer URL to retrieve the offer information and returns it as a DiscoveredCredentialOffer object:

    DiscoveredCredentialOffer structure
    DiscoveredCredentialOffer {
        issuer: String,
        credentials: [OfferedCredential],
        transactionCode: TransactionCode?
    }
    • The application can now use the issuer and credentials properties and present this information for the user to review.
    • The transactionCode property is optional and is only used in the Pre-authorized Code flow when the issuer configures it as part of the issuance flow.
  3. In your MainActivity file, add the following code under the // Claim Credential - Step 3.3: Display discovered credential offer to display the offer details to the user:

    MainActivity.kt
    SharedData.discoveredCredentialOffer?.let { discoveredOffer ->
        Text("Received Credential Offer from ${discoveredOffer.issuer}")
        LazyColumn(Modifier.fillMaxWidth()) {
            items(discoveredOffer.credentials, key = { it.doctype }) { credential ->
                Card(Modifier.fillMaxWidth()) {
                    Column(Modifier.padding(4.dp)) {
                        Text("Name: ${credential.name ?: ""}")
                        Text("DocType: ${credential.doctype}")
                    }
                }
            }
        }
    
        // Claim Credential - Step 4.3: Add transaction code input
    
        // Claim Credential - Step 4.5: Add consent button
    }
  4. Run the app, select the Scan Credential Offer 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 application displays the Credential offer details.

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.

  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 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",
      },
    });
  2. Add the following code under the // Claim a Credential - Step 3.2: Discover Credential Offer comment to call the SDK's discoverCredentialOffer function and discover the credential offer:

    /app/claim-credential.tsx
    const 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's

    discoverCredentialOffer 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 discoverCredentialOffer function makes a request to the uri URL to retrieve the offer information and returns it as an CredentialOfferResponse object:

    CredentialOfferResponse structure
    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) 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.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>
    );
  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 holder application 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. This should then trigger issuing the credential and storing it in the application storage.

Once the user provides their consent by selecting the Consent and retrieve Credential(s) button, your application must call the SDK's retrieveCredentials function to trigger the credential issuance and store the issued credential in the application storage.

  • For Pre-authorized Code offers, this will happen within the application and after the user provided a transaction code (when required by the issuer).
  • For Authorization Code offers, this will happen after the user had completed authentication with the issuer and was redirected back to the application.
  1. Add the following code under the // Claim Credential - Step 4.1: Add retrievedCredentials var comment to add a new variable that will hold the result returned by the SDK's retrieveCredentials method:

    ContentView
    @Published var retrievedCredentials: [MobileCredential] = []
  2. Replace the print statement under the // Claim Credential - Step 4.2: Call retrieveCredential method comment with the following code to create a new function that will call the SDK's retrieveCredentials method:

    ContentView
        Task {
            do {
                let retrievedCredentialResults = try await mobileCredentialHolder.retrieveCredentials(
                    credentialOffer: discoveredCredentialOfferURL,
                    clientId: Constants.clientId,
                    transactionCode: transactionCode
                )
                Task {
                    var credentials: [MobileCredential] = []
                    for result in retrievedCredentialResults {
                        if let credentialId = result.credentialId {
                            if let credential = try? await mobileCredentialHolder.getCredential(credentialId: credentialId) {
                                credentials.append(credential)
                            }
                        }
                    }
                    self.retrievedCredentials = credentials
                    // Clear navigation stack and display retrievedCredentials view
                    navigationPath = NavigationPath()
                    navigationPath.append(NavigationState.retrievedCredentials)
                }
            } catch {
                print(error.localizedDescription)
            }
        }

Let’s review the parameters passed to the retrieveCredentials function:

  • credentialOffer: This is the same credential offer string from the QR Code that we used to call discoverCredentialOffer with.
  • clientId: This was configured when setting up your development environment. It is used by the issuer to identify the wallet application that is making a request to claim credentials.
  • transactionCode: This is only required for Pre-authorized Code credential offers. If your application is only using Authorization Code flows offers, you should set it to nil.

The retrieveCredentials function returns an array of RetrieveCredentialResult objects. Each object contains metadata about a credential that was retrieved:

RetrieveCredentialResult
struct RetrieveCredentialResult {
    let docType: String
    let credentialId: String?
    let error: RetrieveCredentialError?
}

[
   {
      "docType":"org.iso.18013.5.1.mDL",
      "credentialId":"F52084CF-8270-4577-8EDD-23149639D985"
   }
]
  • docType : Identifies the credential type.
  • credentialId : Internally unique identifier of this credential.

After the result is received, your application can retrieve specific credentials by calling the SDK's getCredential method with the credentialId of any retrieved credential.

The SDK's getCredential method returns a MobileCredential object which can be used to display the retrieved credential, its claims and verification status to the user.

Since this object can be used across multiple views, it will make sense to create one view that will represent it. We will use this view in both the Proximity and Online presentation tutorials.

  1. Create a new file named DocumentView and add the following content:

    DocumentView
    import MobileCredentialHolderSDK
    import SwiftUI
    
        struct DocumentView: View {
    
            var viewModel: DocumentViewModel
    
            var body: some View {
                VStack(alignment: .leading, spacing: 10) {
                    Text(viewModel.docType)
                        .font(.title)
                        .fontWeight(.bold)
                        .padding(.bottom, 5)
    
                    ForEach(viewModel.namespacesAndClaims.keys.sorted(), id: \.self) { key in
                        VStack(alignment: .leading, spacing: 5) {
                            Text(key)
                                .font(.headline)
                                .padding(.vertical, 5)
                                .padding(.horizontal, 10)
                                .background(Color.gray.opacity(0.2))
                                .cornerRadius(5)
    
                            ForEach(viewModel.namespacesAndClaims[key]!.keys.sorted(), id: \.self) { claim in
                                HStack {
                                    Text(claim)
                                        .fontWeight(.semibold)
                                    Spacer()
                                    Text(viewModel.namespacesAndClaims[key]![claim]! ?? "")
                                        .fontWeight(.regular)
                                }
                                .padding(.vertical, 5)
                                .padding(.horizontal, 10)
                                .background(Color.white)
                                .cornerRadius(5)
                                .shadow(radius: 1)
                            }
                        }
                        .padding(.vertical, 5)
                    }
                }
                .padding()
                .background(RoundedRectangle(cornerRadius: 10).fill(Color.white).shadow(radius: 5))
                .padding(.horizontal)
            }
        }
    
        // MARK: DocumentViewModel
    
        class DocumentViewModel: ObservableObject {
            var docType: String
    
            var namespacesAndClaims: [String: [String: String?]]
    
            init(from credential: MobileCredential) {
                self.docType = credential.docType
                self.namespacesAndClaims = credential.claims?.reduce(into: [String: [String: String]]()) { result, outerElement in
                    let (outerKey, innerDict) = outerElement
                    result[outerKey] = innerDict.mapValues { $0.textRepresentation }
                } ?? [:]
            }
    
            init(from credentialMetadata: MobileCredentialMetadata) {
                self.docType = credentialMetadata.docType
                var result: [String: [String: String?]] = [:]
                credentialMetadata.claims?.forEach { namespace, claimIDs in
                    var transformedClaims: [String: String?] = [:]
                    claimIDs.forEach { claimID in
                        transformedClaims[claimID] = Optional<String>.none
                    }
                    result[namespace] = transformedClaims
                }
                self.namespacesAndClaims = result
            }
    
            init(from request: MobileCredentialRequest) {
                self.docType = request.docType
                self.namespacesAndClaims = request.namespaces.reduce(into: [String: [String: String?]]()) { result, outerElement in
                    let (outerKey, innerDict) = outerElement
                    result[outerKey] = innerDict.mapValues { _ in nil }
                }
            }
        }
    
        // MARK: Helper
        extension MobileCredentialElementValue {
            var textRepresentation: String {
                switch self {
                case .bool(let bool):
                    return "\(bool)"
                case .string(let string):
                    return string
                case .int(let int):
                    return "\(int)"
                case .unsigned(let uInt):
                    return "\(uInt)"
                case .float(let float):
                    return "\(float)"
                case .double(let double):
                    return "\(double)"
                case let .date(date):
                    let dateFormatter = DateFormatter()
                    dateFormatter.dateStyle = .short
                    dateFormatter.timeStyle = .none
                    return dateFormatter.string(from: date)
                case let .dateTime(date):
                    let dateFormatter = DateFormatter()
                    dateFormatter.dateStyle = .short
                    dateFormatter.timeStyle = .short
                    return dateFormatter.string(from: date)
                case .data(let data):
                    return "Data \(data.count) bytes"
                case .map(let dictionary):
                    let result = dictionary.mapValues { value in
                        value.textRepresentation
                    }
                    return "\(result)"
                case .array(let array):
                    return array.reduce("") { partialResult, element in
                        partialResult + element.textRepresentation
                    }
                    .appending("")
                @unknown default:
                    return "Unknown type"
                }
            }
        }

    The file comprises the following components:

    • DocumentViewModel: This class stores the credentials' docType and claim values.
    • DocumentView: This view takes DocumentViewModel as a parameter and displays its content in a human-readable format.
    • MobileCredentialElementValue : This helper extension allows retrieving a MobileCredentialElementValue from a MobileCredential's claims and present it in a human-readable format.
  2. Return to ContentView and replace EmptyView under the // Claim Credential - Step 4.4: Display retrieved credentials comment with the following code to use the DocumentView structure to display retrieved credentials to the user:

    ContentView
        ScrollView {
            VStack {
                Text("Retrieved Credentials")
                    .font(.title)
                ForEach(viewModel.retrievedCredentials, id: \.id) { credential in
                    DocumentView(viewModel: DocumentViewModel(from: credential))
                }
            }
        }

Once the app calls the retrieveCredentials function, the SDK processes the response based on the type of credential offer retrieved:

  • In the Authorization Code flow:
    • The user is redirected to authenticate with the configured Authentication provider defined in the authorizeEndpoint element of the DiscoveredCredentialOffer object.
    • Upon successful authentication, the user can 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 redirectUri which should be handled by your application.
  • In the Pre-authorized Code flow the user is not redirected out of the application, but rather provides a transaction code (when required by the issuer) and the immediately proceeds to claiming the credential. If no transaction code is required, the user can claim the credential immediately after selecting the Consent and retrieve Credential(s) button.

The issuer then sends the issued mDocs to your application, and the SDK processes and validates them against the ISO/IEC 18013-5:2021 standard. Credentials who meet validation rules are stored in the application internal data storage.

In our example this is achieved by selecting a Consent and retrieve Credential(s) button. Once the user provides their consent by selecting this button, your application must call the retrieveCredentials function to trigger the credential issuance.

  • For Pre-authorized Code offers, this will happen within the application and after the user provided a transaction code (when required by the issuer).
  • For Authorization Code offers, this will happen after the user had completed authentication with the issuer and was redirected back to the application.
  1. In your MainActivity file, add the following code under the // Step 4.1: Add credential issuance configuration comment to configure the redirect URI that will be used to redirect the user back to your application after completing the authentication:

    MainActivity.kt
    credentialIssuanceConfiguration = CredentialIssuanceConfiguration(
        redirectUri = "io.mattrlabs.sample.mobilecredentialtutorialholderapp:" +
                "//credentials/callback",
        autoTrustMobileCredentialIaca = true
    )
    • redirectUri is constructed of the same scheme and host values you have set in the AndroidManifest.xml file.
    • This configuration is only required for the Authorization Code flow offers. If your application is only using Pre-authorized Code flow offers you can remove it.
  2. Add the following code under the // Claim Credential - Step 4.2: Add retrieved credentials variable comment to add a new variable that will hold the result returned by the retrieveCredentials function:

    MainActivity.kt
    var retrievedCredentials: List<MobileCredential> = emptyList()
  3. Add the following code under the // Claim Credential - Step 4.3: Add transaction code input comment to add a new text input field to allow the user to provide a transaction code when one is provided with a Pre-authorized Code flow offer:

    MainActivity.kt
    if (discoveredOffer.transactionCode != null) {
        OutlinedTextField(
         value = transactionCode,
         onValueChange = { transactionCode = it },
         label = { Text("Transaction Code") },
         modifier = Modifier.fillMaxWidth()
        )
    }

    This is only required for Pre-authorized Code flow offers. If your application should only handle Authorization Code flow offers you can remove it.

  4. Add the following code under the // Claim Credential - Step 4.4: Create function to retrieve credentials comment to create a new function that will call the retrieveCredentials method when the user gives the consent for the credentials retrieval:

    MainActivity.kt
    private fun onRetrieveCredentials(
        coroutineScope: CoroutineScope,
        activity: Activity,
        navController: NavController,
        transactionCode: String
    ) {
        coroutineScope.launch {
            try {
                val mdocHolder = MobileCredentialHolder.getInstance()
                val retrieveCredentialResults = mdocHolder.retrieveCredentials(
                    activity,
                    SharedData.scannedOffer!!, 
                    clientId = "android-mobile-credential-tutorial-holder-app", 
                    transactionCode = transactionCode 
                )
    
                // Claim Credential - Step 4.6: Display retrieved credentials
            } catch (e: Exception) {
                Toast.makeText(activity, "Failed to retrieve credentials", Toast.LENGTH_SHORT).show()
            }
        }
    }
    • SharedData.scannedOffer is the same offer String that was used for the discoverCredentialOffer function call.
    • clientId is used by the issuer to recognize the application. This is only used internally in the interaction between the application and the issuer and can be any string as long as it is registered with the issuer as a trusted application.
    • transactionCode is only required for Pre-authorized Code credential offers. If your application is only using Authorization Code flows offers you can remove it.
  5. Add the following code under the // Claim Credential - Step 4.5: Add consent button comment to add a button for calling the new function:

    MainActivity.kt
    Spacer(Modifier.weight(1f))
    Button(
        onClick = {
            onRetrieveCredentials(coroutineScope, activity, navController, transactionCode)
        },
        Modifier.fillMaxWidth()
    ) { Text("Consent and retrieve Credential(s)") }

The retrieveCredentials function then returns a RetrieveCredentialResult list, which references all retrieved credentials:

RetrieveCredentialResult structure
[RetrieveCredentialResult]

@Serializable
data class RetrieveCredentialResult(
    val doctype: String,
    val credentialId: String?,
    val error: RetrieveCredentialError?
)

[
  {
    "doctype":"org.iso.18013.5.1.mDL", 
    "credentialId":"F52084CF-8270-4577-8EDD-23149639D985"
  }
]
  • doctype : Identifies the credential type.
  • credentialId : Unique identifier (UUID) of this credential.

Your application can now retrieve specific credentials by calling the getCredential function with the credentialId of any of the retrieved credentials.

The getCredential function returns a MobileCredential object which represents the issued mDoc, and your application can now introduce UI elements to enable the user to view the credential.

  1. Add the following code under the // Claim Credential - Step 4.6: Display retrieved credentials comment to retrieve the credentials by their IDs from the local storage, save them to the SharedData.retrievedCredentials variable and navigate to the retrievedCredential screen to display the retrieved credential:

    MainActivity.kt
    SharedData.retrievedCredentials = retrieveCredentialResults.mapNotNull {
        try {
            mdocHolder.getCredential(it.credentialId!!, skipStatusCheck = true)
        } catch (e: Exception) {
            val msg = "Failed to get credential from storage"
            Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show()
            null
        }
    }
    
    navController.navigate("retrievedCredential")
    SharedData.discoveredCredentialOffer = null
  2. In your package, create a new file named RetrievedCredentialsScreen.kt.

  3. Add the following code to the new file to display the docType and claims of retrieved credentials to the user:

    RetrievedCredentialsScreen.kt
    import androidx.compose.foundation.background
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.fillMaxWidth
    import androidx.compose.foundation.layout.padding
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.items
    import androidx.compose.foundation.shape.RoundedCornerShape
    import androidx.compose.material3.MaterialTheme
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.unit.dp
    import global.mattr.mobilecredential.common.deviceretrieval.deviceresponse.NameSpace
    import global.mattr.mobilecredential.common.dto.MobileCredentialElement
    
    @Composable
    fun RetrievedCredentialsScreen() {
        if (SharedData.retrievedCredentials.isEmpty()) {
            Text("No credentials received")
        } else {
            Column {
                Text(
                    "Retrieved Credentials",
                    modifier = Modifier.fillMaxWidth(),
                    style = MaterialTheme.typography.titleLarge
                )
    
                LazyColumn(Modifier.fillMaxWidth()) {
                    items(SharedData.retrievedCredentials, key = { it.id }) { credential ->
                        Document(
                            credential.docType,
                            credential.claims.mapValues { (_, claims) ->
                                claims.map { (name, value) -> "$name: ${value.toUiString()}" }.toSet()
                            }
                        )
                    }
                }
            }
        }
    }
    
    @Composable
    fun Document(docType: String, namespacesAndClaims: Map<NameSpace, Set<String>>) {
        Column(Modifier.fillMaxWidth().padding(6.dp)) {
            Text(docType, Modifier.padding(6.dp), style = MaterialTheme.typography.titleMedium)
            namespacesAndClaims.forEach { (namespace, claims) ->
                Text(namespace, Modifier.padding(6.dp), style = MaterialTheme.typography.titleSmall)
                Column(
                    Modifier
                        .padding(6.dp)
                        .fillMaxWidth()
                        .background(MaterialTheme.colorScheme.background, RoundedCornerShape(6.dp))
                        .padding(6.dp)
                ) {
                    claims.forEach { claim -> Text(claim) }
                }
            }
        }
    }
    
    fun MobileCredentialElement.toUiString() = when (this) {
        is MobileCredentialElement.ArrayElement, is MobileCredentialElement.DataElement,
        is MobileCredentialElement.MapElement -> this::class.simpleName ?: "Unknown element"
    
        else -> value.toString()
    }
  4. Back in your MainActivity file, add the following code under the // Claim Credential - Step 4.9: Add "Retrieved Credential" screen call comment to connect the created composable to the navigation graph:

    MainActivity.kt
    RetrievedCredentialsScreen()

Once the app calls the retrieveCredentials function, the SDK processes the response based on the type of credential offer retrieved:

  • In the Authorization Code flow:
    • The user is redirected to authenticate with the configured Authentication provider defined in the authorizeEndpoint element of the DiscoveredCredentialOffer object.
    • Upon successful authentication, the user can 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 redirectUri which should be handled by your application.
  • In the Pre-authorized Code flow the user is not redirected out of the application, but rather provides a transaction code (when required by the issuer) and the immediately proceeds to claiming the credential. If no transaction code is required, the user can claim the credential immediately after selecting the Consent and retrieve Credential(s) button.

The issuer then sends the issued mDocs to your application, and the SDK processes and validates them against the ISO/IEC 18013-5:2021 standard. Credentials who meet validation rules are stored in the application internal data storage.

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.

  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 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 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: Retrieve Credentials comment to call the SDK's retrieveCredentials function to retrieve the offered credentials:

    /app/claim-credential.tsx
    const retrieved = await retrieveCredentials({
      credentialOffer: credentialOffer, 
      clientId: CLIENT_ID, 
      redirectUri: REDIRECT_URI, 
      autoTrustMobileIaca: true, 
    });
    
    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 the CredentialOfferResponse object returned by the discoverCredentialOffer function. 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 holder 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 holder application screen after completing authentication.
    • 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.
  2. 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>

Authentication

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.

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.

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 issuance guides and tutorials 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.

Let's test the end-to-end flow of claiming a credential using the application you had just built.

Step 5: Test the application

Authorization code flow

  1. Run the application.
  2. Select the Scan Credential Offer button.
  3. Scan the following QR code:

QR Code

  1. Select the Consent and retrieve Credential(s) button.

You should see a result similar to the following:

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.

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 provider to comply with the ISO/IEC 18013-5:2021 standard and the OID4VCI specification.

Credential claimed

Pre-authorized Code flow

Let's test the end-to-end flow of claiming a credential using the Pre-authorized Code flow:

  1. Open the MATTR Labs Pre-authorized Offer tool.
  2. Turn on the Transaction Code option for the tool to generate a transaction code.
  3. Select the Generate Credential Offer button.
    A QR code will be generated and rendered on the screen, along with a transaction code.
  4. Run your application.
  5. Select the Scan Credential Offer button.
  6. Scan the QR code.
  7. Enter the transaction code retrieved from the MATTR Labs tool.
  8. Select the Consent and retrieve Credential(s) button.

In production implementations, the transaction code is generated by the issuer and shared with the intended holder by a separate secure channel. In this tutorial, the transaction code is generated by the MATTR Labs tool and displayed on the screen for demonstration purposes and to simplify testing.

Congratulations! Your application can now interact with an OID4VCI Credential offer to claim mDocs!

Summary

You have just used the mDocs Holder SDKs to build an application that can claim an mDoc issued via OID4VCI, supporting both the Authorization Code and Pre-authorized Code flows:

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?

How would you rate this page?