Handling Wallet Responses


Once you've defined the claims for a DID Connect session as shown in the Requesting Claims guide, the next critical step is to process the user's response from their DID Wallet. The @arcblock/did-connect-js library provides a comprehensive set of lifecycle callbacks to manage every possible outcome of a session, whether the user approves, declines, or the session times out.

These callbacks are passed as functions to the handlers.attach() method and give you specific hooks into the authentication process to run your business logic.

The DID Connect Lifecycle#

A typical DID Connect session follows a clear sequence of events. The diagram below illustrates this flow and indicates when each major lifecycle callback is triggered.


Lifecycle Callbacks#

Here is a detailed breakdown of the available callbacks you can use to handle the entire DID Connect session lifecycle.

Callback

Triggered When...

Common Use Cases

onStart

A new session is initiated and the QR code is ready.

Logging session initiation, preparing session-specific resources.

onConnect

The user's wallet scans the QR code and establishes a connection, but before claims are approved.

Permission checks based on userDid, generating dynamic claims.

onDecline

The user explicitly rejects the request in their wallet.

Displaying a cancellation message, logging the rejection.

onAuth

(Required) The user approves the request and submits the requested claims.

Core business logic: validating claims, creating a user session, updating a database.

onComplete

The entire DID Connect session has finished, regardless of success or failure.

Final cleanup tasks, removing temporary session data.

onExpire

The session times out before the user completes the action.

Notifying the user that the QR code has expired, cleaning up stale sessions.

onError

An unexpected error occurs during the session.

Error logging and reporting.

onConnect: Pre-Approval Logic & Dynamic Claims#

The onConnect callback is particularly powerful because it allows you to run logic after you know the user's DID but before they've seen the full list of claims. This is the ideal place to perform permission checks or dynamically generate claims based on the user's identity.

Parameters: { req, challenge, userDid, userPk, extraParams, updateSession }

Example: Generating Dynamic Claims with onConnect

handlers.attach({
  action: 'dynamic-claims',

  // This function can be async
  onConnect: ({ userDid }) => {
    // You can check the user's DID against an allow-list
    if (!isUserAllowed(userDid)) {
      throw new Error('You are not authorized to access this service.');
    }

    // Or return a dynamic set of claims based on user properties
    return {
      profile: () => ({
        fields: ['fullName', 'email'],
        description: 'Please provide your name and email to continue',
      }),
    };
  },

  onAuth: async ({ claims, userDid }) => {
    // `claims` now contains the result for the dynamically generated profile claim
    console.log('Dynamic claim approved by:', userDid, claims);
  },
});

onAuth: Handling Successful Authentication#

The onAuth callback is the heart of your DID Connect integration. It is the only required callback and is executed when the user successfully approves the request in their wallet. It receives the claims data submitted by the user, which you can then use to complete your application's logic, such as logging the user in or verifying ownership of an asset.

Parameters: { req, challenge, claims, userDid, userPk, extraParams, updateSession }

Example: Processing Submitted Claims in onAuth

handlers.attach({
  action: 'profile-login',
  claims: {
    profile: () => ({
      fields: ['fullName', 'email'],
      description: 'Please provide your name and email to log in.',
    }),
  },

  onAuth: async ({ userDid, claims }) => {
    try {
      const profileClaim = claims.find((c) => c.type === 'profile');
      if (profileClaim) {
        console.log(`Login success for ${userDid}`);
        console.log('User Profile:', profileClaim.value);
        // Here you would typically create a user session, save to a database, etc.
        const user = await findOrCreateUser(userDid, profileClaim.value);
      }
    } catch (err) {
      console.error('Error during login process:', err);
    }
  },
});

onDecline: Handling User Cancellation#

If a user decides to cancel the request in their wallet, the onDecline callback is triggered. This allows you to handle the cancellation gracefully, for example, by informing the user on the frontend that the login attempt was canceled.

Parameters: { req, challenge, userDid, userPk, extraParams, updateSession }

Example: Handling a Declined Request

handlers.attach({
  action: 'profile-login',
  claims: { /* ... */ },
  onAuth: async ({ userDid, claims }) => { /* ... */ },

  onDecline: ({ userDid }) => {
    console.log(`User ${userDid} declined the connection request.`);
    // You might update the session to notify the frontend
  },
});

Persisting Data with updateSession#

Several callbacks, including onAuth, onConnect, and onStart, receive an updateSession function as a parameter. This utility is essential for passing information from your backend logic back to the session state, which can then be polled and retrieved by your frontend application.

For example, after a successful login in onAuth, you can generate an application-specific JWT and save it to the session. Your frontend can then retrieve this token to authenticate subsequent API requests.

  • To persist plain, non-sensitive info: await updateSession({ key: 'value' });
  • To persist sensitive info (will be encrypted): await updateSession({ key: 'sensitive-value' }, true);

Example: Using updateSession to Pass a Token to the Frontend

handlers.attach({
  action: 'profile-login',
  claims: { /* ... */ },
  onAuth: async ({ userDid, claims, updateSession }) => {
    const user = await findOrCreateUser(userDid, claims);
    const appToken = generateAppToken(user);

    // Persist a non-sensitive session ID
    await updateSession({ sessionId: 'some-session-id' });

    // Persist a sensitive JWT, which will be encrypted in the session store
    await updateSession({ token: appToken }, true);

    console.log(`Session updated for user ${userDid}`);
  },
});

By effectively using these lifecycle callbacks, you can build robust and user-friendly authentication flows. Now that you understand how to handle responses for a single session, you are ready to explore more complex user journeys.

Next, learn how to create multi-step interactions by linking multiple DID Connect sessions together in the Chaining Workflows guide.