GuidesReact Native Holder SDKπŸŽ“ Proximity presentation

Learn how to build a React Native iOS and Android application that can present an mDoc via a proximity workflow

Introduction

In this tutorial will use the React Native Holder SDK to build iOS and Android applications that can present a claimed mDoc to a verifier that supports proximity verification as per ISO/IEC 18013-5.

Tutorial Workflow

  1. The user launches the wallet application and generates a QR code.
  2. The verifier scans the QR code, connects with the wallet and requests an mDoc for verification.
  3. The wallet displays matching credentials to the user and asks for consent to share them with the verifier.
  4. The verifier receives the wallet’s response and verifies the provided credential.

The result will look something like this:

Prerequisites

Before getting started, let’s make sure you have everything you need.

Prior knowledge

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

Assets

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

This tutorial is intended for use with the latest versions of MATTR’s React Native Holder SDK and the React Native Mobile Credential Holder SDK extension.

Development environment

Prerequisite tutorial

Testing devices

As this tutorial implements a proximity presentation workflow, you will need two separate physical devices to test the end-to-end result:

  • Holder device:
    • Supported iOS and/or Android device to run the built application on, setup with:
      • Biometric authentication.
      • Bluetooth access and Bluetooth turned on.
      • Available internet connection.
  • Verifier device:
    • Android/iOS device with an installed verifier application. We recommend downloading and using the MATTR GO Verify example app.
    • Setup with Bluetooth access and Bluetooth turned on.

Got everything? Let’s get going!

Tutorial steps

To enable a user to present a stored mDoc to a verifier via a proximity presentation workflow, you will build the following capabilities into your application:

  1. Create a QR code for the verifier to scan and establish a secure connection.
  2. Receive and handle a presentation request from the verifier.
  3. Send a matching mDoc presentation to the verifier.

Create a QR code for the verifier to scan

Tutorial Workflow

The first capability you need to build is to establish a secure communication channel between the verifier and holder devices. As defined in ISO/IEC 18130-5:2021, a proximity presentation workflow is always initiated by the holder (wallet user), who must create a QR code for the verifier to scan in order to initiate the device engagement phase.

To achieve this, your wallet application needs a UI element for the user to interact with and trigger this device engagement by calling the SDK’s createProximityPresentationSession method.

  1. In your project’s app directory, create a new file named proximity-presentation.tsx and add the following scaffolding code to create a new screen that will display the generated QR code:

    app/proximity-presentation.tsx
    // Step 2.2: Import Credential selector component
     
    import { useWallet } from "@/providers/WalletProvider";
    import type { PresentationSessionSuccessRequest } from "@mattrglobal/wallet-sdk-react-native";
    import { useRouter } from "expo-router";
    import React, { useState, useCallback, useEffect } from "react";
    import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native";
    import QRCode from "react-native-qrcode-svg";
     
    export default function ProximityPresentation() {
      const router = useRouter();
      const { wallet } = useWallet();
      const [error, setError] = useState<string | null>(null);
      const [deviceEngagement, setDeviceEngagement] = useState<string | null>(null);
      const [requests, setRequests] = useState<PresentationSessionSuccessRequest["request"]>([]);
      const [selectedCredentialIds, setSelectedCredentialIds] = useState<string[]>([]);
     
      const navigateToIndex = useCallback(() => {
        router.push("/");
      }, [router]);
     
      const resetState = useCallback(() => {
        setDeviceEngagement(null);
        setRequests([]);
        setSelectedCredentialIds([]);
      }, []);
     
      const handleError = useCallback((message: string) => {
        setError(message);
        console.error(message);
      }, []);
     
      // Step 2.3: Add handleToggleSelection function
     
     
      if (!wallet) {
        return (
          <View style={styles.container}>
            <Text style={styles.errorText}>Wallet instance not found. Please restart the app and try again.</Text>
          </View>
        );
      }
     
      // Step 1.2: Add handleStartSession function
     
      // Step 1.7: Add terminateSession function
     
      // Step 3.1: Add handleSendResponse function
     
      return (
        <View style={styles.container}>
          {error && <Text style={styles.errorText}>{error}</Text>}
          {/* Step 1.3: Add fallback UI*/}
        </View>
      );
    }
     
    const styles = StyleSheet.create({
      container: { flex: 1, padding: 16, paddingBottom: 32 },
      errorText: { color: "red", marginBottom: 10 },
      infoText: { marginBottom: 10 },
      qrContainer: { alignItems: "center", marginVertical: 20 },
      button: {
        backgroundColor: "#007AFF",
        paddingVertical: 12,
        paddingHorizontal: 20,
        borderRadius: 8,
        alignItems: "center",
        marginTop: 20,
      },
      buttonDanger: {
        backgroundColor: "#FF3B30",
      },
      buttonText: {
        color: "white",
        fontSize: 16,
        fontWeight: "600",
      },
    });

This will serve as the basic structure for this capability. We will copy and paste different code snippets into specific locations in this codebase to achieve the different functionalities. These locations are indicated by comments that reference both the section and the step. We recommend copying and pasting the comment text (e.g. // Proximity Presentation - Step 1.2: Add Proximity Presentation button) to easily locate it in the code.

  1. Add the following code under the // Step 1.2: Add handleStartSession function comment to create a new handleStartSession function that calls the SDK’s createProximityPresentationSession function:

    app/proximity-presentation.tsx
    const handleStartSession = useCallback(async () => {
        try {
            const result = await wallet.credential.mobile.createProximityPresentationSession({
                onRequestReceived: (data) => {
                    if ('error' in data) {
                        handleError(`Request received error: ${data.error}`)
                        return
                    }
                    setRequests(data.request)
                },
                onSessionTerminated: () => {
                    resetState()
                    navigateToIndex()
                }
            })
            if (result.isErr()) {
                throw new Error(`Error creating proximity session: ${JSON.stringify(result.error)}`)
            }
            setDeviceEngagement(result.value.deviceEngagement)
        } catch (err: any) {
            handleError(err.message)
        }
    }, [wallet, handleError, resetState, navigateToIndex])
     
    // Automatically start session when component mounts
    useEffect(() => {
        handleStartSession()
    }, [handleStartSession])
  2. Add the following code under the {/* Step 1.3: Add fallback UI */} comment to display a message to the user while the proximity session is being established:

    app/proximity-presentation.tsx
      {!deviceEngagement ? (
        <>
          <Text style={styles.infoText}>Waiting for session to establish...</Text>
        </>
      ) : (
        <>
          {/* Step 1.4: Display QR code */}
        </>
      )}

    Now, when the user navigates to the Proximity Presentation screen, the handleStartSession function is called and creates a new proximity presentation session by calling the SDK’s createProximityPresentationSession function. This SDK function returns a ProximityPresentationSession instance that includes a deviceEngagement string in Base64 format:

    "mdoc:owBjMS4wAYIB2BhYS6QBAiABIVgghaBYJe7KSqcEolhmnIJaYJ2AIevkKbEy5xP7tkwlqAwiWCAMGCGe6uFI2hKeghb59h_K4hPV-Ldq6vnaxsRiySMH9gKBgwIBowD0AfULUKRoj0ZH60Qco-m0k97qRSQ"

    The deviceEngagement string is always prefixed with mdoc: and contains the information required to establish a secure connection between the two devices, including:

    • Wireless communication protocols supported by the holder.
    • How the verifier can connect with the holder.
    • Ephemeral public key which is unique to the current session.
    • Additional feature negotiations.

    Your app needs to convert this deviceEngagement string into a QR code and display it so that the verifier can scan it.

  3. Add the following code under the {/* Step 1.4: Display QR code */} comment to use the deviceEngagement variable to generate a QR code and display it:

    app/proximity-presentation.tsx
      <>
        <Text style={styles.infoText}>A proximity session is active.</Text>
        {requests.length === 0 ? (
          <View style={styles.qrContainer}>
            <QRCode value={deviceEngagement} size={200} />
            {/* Step 1.8: Add Terminate session button */}
          </View>
        ) : (
          <>
          {/* Step 2.4: Display request details */}
     
          {/* Step 3.2: Send response */}
          </>
        )}
      </>
  4. Open the index.tsx file in the app directory and add the following code under the {/* Proximity Presentation - Step 1.5: Add Proximity Presentation button */} comment to add a new button to navigate to the new proximity-presentation screen created in the previous steps:

    app/index.tsx
    <TouchableOpacity style={styles.button} onPress={() => router.replace("/proximity-presentation")}>
      <Text style={styles.buttonText}>Proximity Presentation</Text>
    </TouchableOpacity>

Proximity verification workflows require the use of Bluetooth to establish a secure communication channel with a verifier device. To enable this feature for iOS applications, you need to adjust the app configuration to include the necessary permissions.

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

To properly manage proximity sessions, we will create two functions that allow the application to gracefully handle scenarios where users navigate away from the screen before completing the verification process.

  1. Add the following code under the {/* Proximity Presentation - Step 1.7: Add terminateSession function */} comment to create the terminateSession and handleTerminateSession functions:

    app/proximity-presentation.tsx
    const terminateSession = useCallback(async () => {
        try {
            await wallet.credential.mobile.terminateProximityPresentationSession()
            resetState()
            navigateToIndex()
        } catch (err: any) {
            handleError(`Failed to terminate session: ${err.message}`)
        }
    }, [wallet, resetState, handleError, navigateToIndex])
     
    const handleTerminateSession = useCallback(async () => {
        await terminateSession()
    }, [terminateSession])
     
    useEffect(() => {
        return () => {
            if (deviceEngagement) {
                terminateSession()
            }
        }
    }, [deviceEngagement, terminateSession])
  2. Add the following code under the {/* Proximity Presentation - Step 1.8: Add Terminate session button */} comment to create a button that will call the handleTerminateSession function when selected:

    app/proximity-presentation.tsx
    <TouchableOpacity style={[styles.button, styles.buttonDanger]} onPress={handleTerminateSession}>
        <Text style={styles.buttonText}>Terminate Session</Text>
    </TouchableOpacity>

    The handleTerminateSession function is called when the user selects the Terminate Session button, and it displays an alert to confirm that the session has been terminated.

    The useEffect hook acts as a cleanup function when the component is unmounted or if the user navigates away from the screen. If a proximity session is active, it calls the terminateSession function to end the session.

    The terminateSession function calls the SDK’s terminateProximityPresentationSession method to end the current proximity presentation session and reset the application state.

  3. Manually delete the ios folder from your application folder. This is required for the permissions changes we performed to take effect.

  4. Run the app and select the Proximity Presentation button. You should see a result similar to the following:

  • As the user selects the Proximity Presentation button a new proximity presentation session is created.

  • Once the session is created, the application retrieves the deviceEngagement string and uses it to generate and display a QR code that can be scanned by a verifier device to establish a secure proximity communication channel over Bluetooth.

  • Once the QR code is displayed, the createProximityPresentationSession function enters a listening state, ready to establish a Bluetooth connection with a verifier application that scans the QR Code.

  • When a verifier application scans the QR code, the devices will automatically exchange public keys to establish a secure communication channel, enabling the verifier to send a presentation request, which details:

    • What credentials are required.
    • What specific claims are required from these credentials.

    We will implement the logic that handles this presentation request in the next step.

Tutorial Workflow

Handle a presentation request

Tutorial Workflow

The createProximityPresentationSession function can handle three types of events:

  • onConnected: When a secure connection is established.
  • onSessionTerminated: When a secure connection is terminated for whatever reason.
  • onRequestReceived: When a presentation request is received from the verifier.

onConnected and onSessionTerminated are optional events and will not be implemented in this tutorial. Check out our SDK Docs for a complete description of these events and how you can handle them.

When the SDK receives a presentation request from a verifier, an onRequestReceived event is triggered. The SDK then checks its credential storage for any credentials that match the information defined in this request.

The application then needs to:

  • Present these matching credentials to the user.
  • Present what claims will be shared with the verifier.
  • Provide a UI element for the user to consent sharing this information with the verifier.

The following step is also included in the Online presentation tutorial. If you had already completed this tutorial and created the RequestCredentialSelector.tsx component, you may skip to step 2.2.

  1. In the app/components directory, create a file named RequestCredentialSelector.tsx and add the following code:
app/components/RequestCredentialSelector.tsx
import type { MobileCredentialMetadata, PresentationSessionSuccessRequest } from "@mattrglobal/wallet-sdk-react-native";
import type React from "react";
import { FlatList, type ListRenderItem, StyleSheet, Text, TouchableOpacity, View } from "react-native";
 
type RequestCredentialSelectorProps = {
  requests: PresentationSessionSuccessRequest["request"];
  selectedCredentialIds: string[];
  onToggleSelection: (credentialId: string) => void;
};
 
type RequestItem = PresentationSessionSuccessRequest["request"][number];
 
/**
 * Component that renders a list of credential requests and their matched credentials.
 *
 * @param props - The component props.
 * @param props.requests - The list of credential requests.
 * @param props.selectedCredentialIds - The list of selected credential IDs.
 * @param props.onToggleSelection - Callback function to toggle the selection of a credential.
 * @returns The rendered component.
 */
export default function RequestCredentialSelector({
  requests,
  selectedCredentialIds,
  onToggleSelection,
}: RequestCredentialSelectorProps) {
  const renderCredential: ListRenderItem<MobileCredentialMetadata> = ({ item: cred }) => {
    const isSelected = selectedCredentialIds.includes(cred.id);
    return (
      <TouchableOpacity style={styles.credentialItem} onPress={() => onToggleSelection(cred.id)}>
        <View style={styles.selectionIndicator}>{isSelected && <View style={styles.selectionInner} />}</View>
        <Text style={styles.credentialText}>
          {cred.branding?.name ?? "Credential"} ({cred.id})
        </Text>
      </TouchableOpacity>
    );
  };
 
  const renderRequest: ListRenderItem<RequestItem> = ({ item }) => (
    <View style={styles.requestContainer}>
      <Text style={styles.label}>Request Details</Text>
      <Text style={styles.requestInfo}>
        {typeof item.request === "object" ? JSON.stringify(item.request, null, 2) : item.request}
      </Text>
      <Text style={styles.label}>Matched Credentials:</Text>
      <FlatList
        data={item.matchedCredentials}
        keyExtractor={(cred) => cred.id}
        renderItem={renderCredential}
        style={styles.credentialsList}
        contentContainerStyle={styles.credentialsListContent}
      />
    </View>
  );
 
  return (
    <FlatList
      data={requests}
      keyExtractor={(_, idx) => idx.toString()}
      renderItem={renderRequest}
      style={styles.requestsList}
      contentContainerStyle={styles.requestsListContent}
    />
  );
}
 
const styles = StyleSheet.create({
  requestsList: {
    flex: 1,
  },
  requestsListContent: {
    paddingBottom: 10,
  },
  requestContainer: {
    backgroundColor: "#f0f0f0",
    padding: 10,
    borderRadius: 8,
    marginBottom: 15,
  },
  requestInfo: {
    fontStyle: "italic",
    fontSize: 12,
    marginBottom: 10,
  },
  label: {
    fontWeight: "bold",
    fontSize: 14,
    textTransform: "uppercase",
    marginBottom: 5,
  },
  credentialsList: {
    maxHeight: 200,
  },
  credentialsListContent: {
    paddingBottom: 10,
  },
  credentialItem: {
    flexDirection: "row",
    alignItems: "center",
    marginBottom: 8,
    paddingVertical: 4,
  },
  selectionIndicator: {
    height: 20,
    width: 20,
    borderRadius: 10,
    borderWidth: 1,
    borderColor: "#000",
    alignItems: "center",
    justifyContent: "center",
    marginRight: 8,
  },
  selectionInner: {
    height: 10,
    width: 10,
    borderRadius: 5,
    backgroundColor: "#000",
  },
  credentialText: {
    fontSize: 16,
  },
});

This component displays all existing credentials that match the verification request, and provides a UI for the user to select the credential they wish to share.

Identifiers of the selected credentials are assigned to the selectedCredentialIds variable, making them available for use in the next steps.

  1. Add the following code under the // Step 2.2: Import Credential selector component comment in the proximity-presentation.tsx file to import the RequestCredentialSelector component created in the previous step:
app/proximity-presentation.tsx
import RequestCredentialSelector from '@/components/RequestCredentialSelector'

Before we can display and use the RequestCredentialSelector component, we need to implement a functionality that allows users to select which credentials they want to share.

The handleToggleSelection function updates the selectedCredentialIds state array when users tap on credentials. This function is passed to the RequestCredentialSelector component to handle selection state, and the resulting array of selected credential identifiers will be used when sending the presentation response to the verifier.

  1. Add the following code under the // Step 2.3: Add handleToggleSelection function comment to create the handleToggleSelection function:
app/proximity-presentation.tsx
const handleToggleSelection = useCallback((id: string) => {
    setSelectedCredentialIds(
        (prev) =>
            prev.includes(id)
                ? prev.filter((item) => item !== id) // Remove if already selected
                : [...prev, id] // Add if not selected
    )
}, [])
  1. Add the following code under the {/* Step 2.4: Display request details */} comment to display the RequestCredentialSelector component to the user, enabling them to select which credentials to share:
app/proximity-presentation.tsx
  <RequestCredentialSelector
    requests={requests}
    selectedCredentialIds={selectedCredentialIds}
    onToggleSelection={handleToggleSelection}
  />
  1. Run the app, select the Proximity Presentation button and the use your verifier testing device to scan the displayed QR code. You should see a result similar to the following:
  • As the user selects the Proximity Presentation button a new proximity presentation session is created and a QR code is displayed.

  • When a verifier application scans the QR code, the devices will automatically exchange public keys to establish a secure communication channel, enabling the verifier to send a presentation request, which details:

    • What credentials are required.
    • What specific claims are required from these credentials.
  • The request details are then displayed by the RequestCredentialSelector component and enable the user to select which matching credential to share with the verifier.

    We will implement the logic to share the information with the verifier in the next step.

Send a presentation response

Tutorial Workflow

The next (and final!) capability you need to build is for your application to send a presentation response upon receiving consent from the user to share information with the verifier.

Once the user provides this consent your application should call the SDK’s sendProximityPresentationResponse function to share the selected credentials with the verifier as a presentation response.

  1. Add the following code under the // Step 3.1: Add handleSendResponse function comment to create a new handleSendResponse function that calls the SDK’s sendProximityPresentationResponse function:

    app/proximity-presentation.tsx
    const handleSendResponse = useCallback(async () => {
        if (selectedCredentialIds.length === 0) {
            Alert.alert('No Credentials Selected', 'Please select at least one credential to send.')
            return
        }
        try {
            const result = await wallet.credential.mobile.sendProximityPresentationResponse({
                credentialIds: selectedCredentialIds
            })
            if (result.isErr()) {
                await terminateSession()
                throw new Error(`Error sending proximity response: ${result.error}`)
            }
            Alert.alert('Success', 'Presentation response sent successfully!')
            navigateToIndex()
        } catch (err: any) {
            handleError(err.message)
        }
    }, [wallet, selectedCredentialIds, handleError, terminateSession])
  2. Add the following code under the {/* Step 3.2: Send response */} comment to add a new button that calls the handleSendResponse function created in the previous step:

    app/proximity-presentation.tsx
    <TouchableOpacity style={styles.button} onPress={handleSendResponse}>
      <Text style={styles.buttonText}>Share credential</Text>
    </TouchableOpacity>

    The sendProximityPresentationResponse function signs the presentation response with the user’s device private key (to prove Device authentication) and shares it as an encoded CBOR file.

Test the application

Run the app and perform the following:

  1. Navigate to the Proximity Presentation screen.
  2. Select the Present Credential button.
  3. Use your testing verifier app to scan the presented QR code and send a presentation request.
  4. Back on the holder device, select the matching credential to share.
  5. Select the Share credential button.

You should see a result similar to the following:

As the user selects the credential to share, the verifier app will receive the presentation response, verify any incoming credentials and display the verification results.

Congratulations, you have now completed this tutorial, and should have a working wallet application that can claim an mDoc using an OID4VCI workflow, and present it to a verifier for proximity verification via Bluetooth.

Summary

You have just used the React Native Holder SDK to build an iOS and Android application that can present a claimed mDoc to a verifier via a proximity presentation workflow as per ISO/IEC 18013-5.

Tutorial Workflow

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

  • Generating a QR code for the verifier to scan and establish a secure communication channel.
  • Receive and handle a presentation request from the verifier.
  • Display matching credentials to the user and ask for consent to share them with the verifier.
  • Send matching credentials to the verifier as a presentation response.

What’s next?