Skip to main content

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.

Handling Wallet Responses

Lifecycle Callbacks

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

CallbackTriggered When...Common Use Cases
onStartA new session is initiated and the QR code is ready.Logging session initiation, preparing session-specific resources.
onConnectThe user's wallet scans the QR code and establishes a connection, but before claims are approved.Permission checks based on userDid, generating dynamic claims.
onDeclineThe 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.
onCompleteThe entire DID Connect session has finished, regardless of success or failure.Final cleanup tasks, removing temporary session data.
onExpireThe session times out before the user completes the action.Notifying the user that the QR code has expired, cleaning up stale sessions.
onErrorAn 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

javascript
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

javascript
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

javascript
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

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