๐ŸŽฏ Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. You are here โ†’ Data Signing Flow Implementation

Welcome to the REL-ID Data Signing codelab! This tutorial builds upon your existing MFA implementation to add secure cryptographic data signing capabilities using REL-ID SDK's authentication and signing infrastructure.

What You'll Build

In this codelab, you'll enhance your existing MFA application with:

What You'll Learn

By completing this codelab, you'll master:

  1. Data Signing API Integration: Implementing authenticateUserAndSignData() with proper parameter handling
  2. Authentication Levels & Types: Understanding and implementing RDNAAuthLevel and RDNAAuthenticatorType enums
  3. Step-Up Authentication: Handling password challenges during sensitive signing operations
  4. Event-Driven Architecture: Managing onAuthenticateUserAndSignData callbacks and state updates

Prerequisites

Before starting this codelab, ensure you have:

Get the Code from GitHub

The code to get started can be found in a GitHub repository.

You can clone the repository using the following command:

git clone https://github.com/uniken-public/codelab-cordova.git

Navigate to the relid-data-signing folder in the repository you cloned earlier

Codelab Architecture Overview

This codelab extends your MFA application with comprehensive data signing functionality:

  1. REL-ID SDK Integration: Direct integration with REL-ID data signing APIs via Cordova plugin
  2. Multi-Level Authentication: Support for REL-ID authentication levels 0, 1, and 4 only
  3. Step-Up Authentication Flow: Password challenges and biometric authentication
  4. Event-Driven Integration: Seamless integration with REL-ID SDK's callback architecture
  5. State Management: Proper cleanup using resetAuthenticateUserAndSignDataState API

Before implementing data signing functionality, let's understand the cryptographic concepts and security architecture that powers REL-ID's data signing capabilities.

What is Cryptographic Data Signing?

Data signing is a cryptographic process that creates a digital signature for a piece of data, providing:

REL-ID Data Signing Architecture

REL-ID's data signing implementation follows enterprise security standards:

User Data Input โ†’ Authentication Challenge โ†’ LDA/Password Verification โ†’
Cryptographic Signing โ†’ Signed Payload โ†’ Verification

Key Security Features

  1. Multi-Level Authentication: 3 supported authentication levels (0, 1, 4) for different security requirements
  2. Supported Authenticator Types: NONE (auto-selection) and IDV Server Biometric only
  3. Step-Up Authentication: Additional password challenges for sensitive operations
  4. Secure State Management: Proper cleanup and reset to prevent authentication bypass
  5. Comprehensive Error Handling: Detailed error codes and recovery mechanisms

When to Use Data Signing

Data signing is ideal for:

Security Considerations

Key security guidelines:

  1. Authentication Level Selection: Match authentication level to data sensitivity
  2. Proper State Cleanup: Always reset authentication state after completion or cancellation
  3. Error Handling: Never expose sensitive error details to end users
  4. Audit Logging: Log all signing attempts for security monitoring
  5. User Experience: Balance security with usability for optimal user adoption

Let's explore the three core APIs that power REL-ID's data signing functionality, understanding their parameters, responses, and integration patterns.

authenticateUserAndSignData API

This is the primary API for initiating cryptographic data signing with user authentication.

API Signature

com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData(
  successCallback,
  errorCallback,
  [payload, authLevel, authenticatorType, reason]
)

Parameters Deep Dive

Parameter

Type

Required

Description

payload

string

โœ…

The data to be cryptographically signed (max 500 characters)

authLevel

number

โœ…

Authentication security level (0, 1, or 4 only)

authenticatorType

number

โœ…

Type of authentication method (0 or 1 only)

reason

string

โœ…

Human-readable reason for signing (max 100 characters)

Official REL-ID Data Signing Authentication Mapping

RDNAAuthLevel

RDNAAuthenticatorType

Supported Authentication

Description

NONE (0)

NONE (0)

No Authentication

No authentication required - NOT RECOMMENDED for production

RDNA_AUTH_LEVEL_1 (1)

NONE (0)

Device biometric, Device passcode, or Password

Priority: Device biometric โ†’ Device passcode โ†’ Password

RDNA_AUTH_LEVEL_2 (2)

NOT SUPPORTED

โŒ SDK will error out

Level 2 is not supported for data signing

RDNA_AUTH_LEVEL_3 (3)

NOT SUPPORTED

โŒ SDK will error out

Level 3 is not supported for data signing

RDNA_AUTH_LEVEL_4 (4)

RDNA_IDV_SERVER_BIOMETRIC (1)

IDV Server Biometric

Maximum security - Any other authenticator type will cause SDK error

How to Use AuthLevel and AuthenticatorType

REL-ID data signing supports three authentication modes:

1. No Authentication (Level 0)

authLevel: 0,  // NONE
authenticatorType: 0  // NONE

2. Re-Authentication (Level 1)

authLevel: 1,  // RDNA_AUTH_LEVEL_1
authenticatorType: 0  // NONE

3. Step-up Authentication (Level 4)

authLevel: 4,  // RDNA_AUTH_LEVEL_4
authenticatorType: 1  // RDNA_IDV_SERVER_BIOMETRIC

Cordova Plugin Call Pattern

// Call signature (callbacks first, parameters in array)
com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData(
  (response) => {
    // Success callback - response is JSON string
    const result = JSON.parse(response);
    console.log('Signing initiated successfully');
  },
  (error) => {
    // Error callback - error is JSON string
    const result = JSON.parse(error);
    console.error('Signing failed:', result.error);
  },
  [payload, authLevel, authenticatorType, reason]  // Parameters as array
);

Implementation Example

// Service layer implementation (from www/src/uniken/services/rdnaService.js)

/**
 * Authenticates user and signs data payload
 * @param {string} payload - Data to sign (max 500 chars)
 * @param {number} authLevel - Authentication level (0, 1, or 4)
 * @param {number} authenticatorType - Authenticator type (0 or 1)
 * @param {string} reason - Signing reason (max 100 chars)
 * @returns {Promise} Resolves with sync response
 */
async authenticateUserAndSignData(payload, authLevel, authenticatorType, reason) {
  return new Promise((resolve, reject) => {
    console.log('RdnaService - Initiating data signing:', {
      payloadLength: payload.length,
      authLevel,
      authenticatorType,
      reason,
    });

    com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData(
      (response) => {
        // Success callback - parse JSON string
        const result = JSON.parse(response);
        console.log('RdnaService - AuthenticateUserAndSignData sync response success');

        if (result.error && result.error.longErrorCode === 0) {
          resolve(result);
        } else {
          reject(result);
        }
      },
      (error) => {
        // Error callback - parse JSON string
        const result = JSON.parse(error);
        console.error('RdnaService - AuthenticateUserAndSignData sync response error:', result);
        reject(result);
      },
      [payload, authLevel, authenticatorType, reason]
    );
  });
}

resetAuthenticateUserAndSignDataState API

This API cleans up authentication state after signing completion or cancellation.

API Signature

com.uniken.rdnaplugin.RdnaClient.resetAuthenticateUserAndSignDataState(
  successCallback,
  errorCallback
)

When to Use

Implementation Example

// Service layer cleanup implementation (from www/src/uniken/services/rdnaService.js)

/**
 * Resets the data signing authentication state
 * @returns {Promise} Resolves when state is reset
 */
async resetAuthenticateUserAndSignDataState() {
  return new Promise((resolve, reject) => {
    console.log('RdnaService - Resetting data signing authentication state');

    com.uniken.rdnaplugin.RdnaClient.resetAuthenticateUserAndSignDataState(
      (response) => {
        const result = JSON.parse(response);
        console.log('RdnaService - ResetAuthenticateUserAndSignDataState sync response success');

        if (result.error && result.error.longErrorCode === 0) {
          resolve(result);
        } else {
          reject(result);
        }
      },
      (error) => {
        const result = JSON.parse(error);
        console.error('RdnaService - ResetAuthenticateUserAndSignDataState sync response error:', result);
        reject(result);
      }
    );
  });
}

onAuthenticateUserAndSignData Event

This callback event delivers the final signing results after authentication completion.

Event Response Structure

/**
 * @typedef {Object} DataSigningResponse
 * @property {string} dataPayload - Original payload that was signed
 * @property {number} dataPayloadLength - Length of the payload
 * @property {string} reason - Reason provided for signing
 * @property {string} payloadSignature - Cryptographic signature (base64)
 * @property {string} dataSignatureID - Unique signature identifier
 * @property {number} authLevel - Authentication level used
 * @property {number} authenticationType - Authentication type used
 * @property {Object} status - Operation status
 * @property {Object} error - Error details (if any)
 */

Event Handler Implementation

// From www/src/uniken/services/rdnaEventManager.js

/**
 * Handles onAuthenticateUserAndSignData event
 * @param {Event} event - DOM event with signing response
 */
onAuthenticateUserAndSignData(event) {
  console.log('RdnaEventManager - onAuthenticateUserAndSignData event received');

  try {
    // Parse event.response (JSON string from plugin)
    const response = JSON.parse(event.response);
    console.log('Data signing response:', JSON.stringify({
      statusCode: response.status?.statusCode,
      errorCode: response.error?.longErrorCode,
      signatureLength: response.payloadSignature?.length
    }, null, 2));

    // Call registered handler
    if (this.dataSigningResponseHandler) {
      this.dataSigningResponseHandler(response);
    }
  } catch (error) {
    console.error('RdnaEventManager - Failed to parse onAuthenticateUserAndSignData:', error);
  }
}

// Handler registration
setDataSigningResponseHandler(callback) {
  this.dataSigningResponseHandler = callback;
}

Success vs Error Handling

Success Indicators:

Error Handling:

Now let's implement the service layer architecture that provides clean abstraction over REL-ID SDK data signing APIs. This follows established Cordova patterns with singleton services and JSDoc types.

RdnaService Integration

First, let's examine the core SDK service implementation. This should already exist in your codelab from MFA implementation:

// www/src/uniken/services/rdnaService.js

/**
 * Authenticates user and signs data payload
 *
 * This method initiates the data signing flow with step-up authentication.
 * It requires user authentication and cryptographically signs the provided
 * payload upon successful authentication.
 *
 * @param {string} payload - Data to sign (max 500 chars)
 * @param {number} authLevel - Authentication level (0, 1, or 4)
 * @param {number} authenticatorType - Authenticator type (0 or 1)
 * @param {string} reason - Signing reason (max 100 chars)
 * @returns {Promise<Object>} Sync response with error details
 */
async authenticateUserAndSignData(payload, authLevel, authenticatorType, reason) {
  return new Promise((resolve, reject) => {
    console.log('RdnaService - Initiating data signing:', {
      payloadLength: payload.length,
      authLevel,
      authenticatorType,
      reason,
    });

    com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData(
      (response) => {
        console.log('RdnaService - AuthenticateUserAndSignData sync callback received');
        const result = JSON.parse(response);

        if (result.error && result.error.longErrorCode === 0) {
          console.log('RdnaService - AuthenticateUserAndSignData sync response success');
          resolve(result);
        } else {
          console.error('RdnaService - AuthenticateUserAndSignData sync response error:', result);
          reject(result);
        }
      },
      (error) => {
        const result = JSON.parse(error);
        console.error('RdnaService - AuthenticateUserAndSignData error callback:', result);
        reject(result);
      },
      [payload, authLevel, authenticatorType, reason]
    );
  });
}

/**
 * Resets the data signing authentication state
 * @returns {Promise<Object>} Sync response
 */
async resetAuthenticateUserAndSignDataState() {
  return new Promise((resolve, reject) => {
    console.log('RdnaService - Resetting data signing authentication state');

    com.uniken.rdnaplugin.RdnaClient.resetAuthenticateUserAndSignDataState(
      (response) => {
        console.log('RdnaService - ResetAuthenticateUserAndSignDataState sync callback received');
        const result = JSON.parse(response);

        if (result.error && result.error.longErrorCode === 0) {
          console.log('RdnaService - ResetAuthenticateUserAndSignDataState sync response success');
          resolve(result);
        } else {
          console.error('RdnaService - ResetAuthenticateUserAndSignDataState sync response error:', result);
          reject(result);
        }
      },
      (error) => {
        const result = JSON.parse(error);
        console.error('RdnaService - ResetAuthenticateUserAndSignDataState error callback:', result);
        reject(result);
      }
    );
  });
}

DataSigningService - High-Level Service Wrapper

Create a high-level service that combines multiple concerns with user-friendly methods:

// www/src/tutorial/screens/dataSigning/DataSigningService.js

/**
 * High-level service for data signing operations
 * Combines rdnaService and DropdownDataService for complete functionality
 */
class DataSigningServiceClass {

  /**
   * Get dropdown data service instance
   */
  get dropdownService() {
    return window.DropdownDataService;
  }

  /**
   * Get RDNA service instance
   */
  get rdnaService() {
    return rdnaService;
  }

  /**
   * Initiates data signing with enum conversion
   *
   * @param {string} payload - Data to sign (max 500 chars)
   * @param {string} authLevelDisplay - Display value (e.g., "RDNA_AUTH_LEVEL_4 (4)")
   * @param {string} authenticatorTypeDisplay - Display value
   * @param {string} reason - Signing reason (max 100 chars)
   * @returns {Promise<Object>} Sync response
   */
  async signData(payload, authLevelDisplay, authenticatorTypeDisplay, reason) {
    console.log('DataSigningService - Starting data signing process');

    try {
      // Convert display values to numeric enums
      const authLevel = this.dropdownService.convertAuthLevelToNumber(authLevelDisplay);
      const authenticatorType = this.dropdownService.convertAuthenticatorTypeToNumber(authenticatorTypeDisplay);

      console.log('DataSigningService - Converted enums:', JSON.stringify({
        authLevel,
        authenticatorType
      }, null, 2));

      // Call SDK API
      const response = await rdnaService.authenticateUserAndSignData(
        payload,
        authLevel,
        authenticatorType,
        reason
      );

      console.log('DataSigningService - Data signing initiated successfully');
      return response;
    } catch (error) {
      console.error('DataSigningService - Data signing failed:', error);
      throw error;
    }
  }

  /**
   * Submit password for step-up authentication
   * @param {string} password - User password
   * @param {number} challengeMode - Challenge mode from getPassword event
   * @returns {Promise<Object>} Sync response
   */
  async submitPassword(password, challengeMode) {
    console.log('DataSigningService - Submitting password for data signing');

    try {
      const response = await rdnaService.setPassword(password, challengeMode);
      console.log('DataSigningService - Password submitted successfully');
      return response;
    } catch (error) {
      console.error('DataSigningService - Password submission failed:', error);
      throw error;
    }
  }

  /**
   * Reset data signing state (cleanup)
   * @returns {Promise<void>}
   */
  async resetState() {
    console.log('DataSigningService - Resetting data signing state');

    try {
      await rdnaService.resetAuthenticateUserAndSignDataState();
      console.log('DataSigningService - State reset successfully');
    } catch (error) {
      console.error('DataSigningService - State reset failed:', error);
      // Don't throw - cleanup should not fail
    }
  }

  /**
   * Validate signing form input
   * @param {string} payload
   * @param {string} authLevel
   * @param {string} authenticatorType
   * @param {string} reason
   * @returns {{isValid: boolean, errors: string[]}}
   */
  validateSigningInput(payload, authLevel, authenticatorType, reason) {
    const errors = [];

    // Validate payload
    if (!payload || payload.trim().length === 0) {
      errors.push('Payload is required');
    } else if (payload.length > 500) {
      errors.push('Payload must be less than 500 characters');
    }

    // Validate auth level
    if (!authLevel || authLevel === '') {
      errors.push('Please select an authentication level');
    } else if (!this.dropdownService.isValidAuthLevel(authLevel)) {
      errors.push('Invalid authentication level');
    }

    // Validate authenticator type
    if (!authenticatorType || authenticatorType === '') {
      errors.push('Please select an authenticator type');
    } else if (!this.dropdownService.isValidAuthenticatorType(authenticatorType)) {
      errors.push('Invalid authenticator type');
    }

    // Validate reason
    if (!reason || reason.trim().length === 0) {
      errors.push('Reason is required');
    } else if (reason.length > 100) {
      errors.push('Reason must be less than 100 characters');
    }

    return {
      isValid: errors.length === 0,
      errors
    };
  }

  /**
   * Format signing response for display
   * @param {Object} response - Raw signing response
   * @returns {Object} Display-formatted response
   */
  formatSigningResultForDisplay(response) {
    return {
      authLevel: response.authLevel?.toString() || 'N/A',
      authenticationType: response.authenticationType?.toString() || 'N/A',
      dataPayloadLength: response.dataPayloadLength?.toString() || 'N/A',
      dataPayload: response.dataPayload || 'N/A',
      payloadSignature: response.payloadSignature || 'N/A',
      dataSignatureID: response.dataSignatureID || 'N/A',
      reason: response.reason || 'N/A'
    };
  }

  /**
   * Convert to result info items array
   * @param {Object} displayData
   * @returns {Array<{name: string, value: string}>}
   */
  convertToResultInfoItems(displayData) {
    return [
      { name: 'Payload Signature', value: displayData.payloadSignature },
      { name: 'Data Signature ID', value: displayData.dataSignatureID },
      { name: 'Reason', value: displayData.reason },
      { name: 'Data Payload', value: displayData.dataPayload },
      { name: 'Auth Level', value: displayData.authLevel },
      { name: 'Authentication Type', value: displayData.authenticationType },
      { name: 'Data Payload Length', value: displayData.dataPayloadLength }
    ];
  }

  /**
   * Get user-friendly error message for error codes
   * @param {number} errorCode
   * @returns {string}
   */
  getErrorMessage(errorCode) {
    switch (errorCode) {
      case 0:
        return 'Success';
      case 214:
        return 'Authentication method not supported. Please try a different authentication type.';
      case 102:
        return 'Authentication failed. Please check your credentials and try again.';
      case 153:
        return 'Operation cancelled by user.';
      default:
        return `Operation failed with error code: ${errorCode}`;
    }
  }
}

// Export singleton instance
const DataSigningService = new DataSigningServiceClass();
window.DataSigningService = DataSigningService;

DropdownDataService - Enum Management

Create a service to manage dropdown data and enum conversions:

// www/src/tutorial/screens/dataSigning/DropdownDataService.js

/**
 * Service for managing dropdown data and enum conversions
 */
class DropdownDataServiceClass {

  /**
   * Get authentication level options for dropdown
   * Only includes levels supported for data signing: 0, 1, and 4
   * @returns {Array<{value: string, description: string}>}
   */
  getAuthLevelOptions() {
    return [
      { value: 'NONE (0)', description: 'No Authentication' },
      { value: 'RDNA_AUTH_LEVEL_1 (1)', description: 'Re-Authentication' },
      { value: 'RDNA_AUTH_LEVEL_4 (4)', description: 'Maximum Security (Recommended)' }
      // Note: Levels 2 and 3 are NOT SUPPORTED for data signing
    ];
  }

  /**
   * Get authenticator type options for dropdown
   * Only includes types supported for data signing: 0 and 1
   * @returns {Array<{value: string, description: string}>}
   */
  getAuthenticatorTypeOptions() {
    return [
      { value: 'NONE (0)', description: 'Auto-select best available' },
      { value: 'RDNA_IDV_SERVER_BIOMETRIC (1)', description: 'Server Biometric Verification' }
      // Note: RDNA_AUTH_PASS (2) and RDNA_AUTH_LDA (3) are NOT SUPPORTED for data signing
    ];
  }

  /**
   * Convert auth level display value to number
   * @param {string} displayValue - e.g., "RDNA_AUTH_LEVEL_4 (4)"
   * @returns {number} Numeric value (0, 1, or 4)
   */
  convertAuthLevelToNumber(displayValue) {
    if (displayValue.includes('(0)')) return 0;
    if (displayValue.includes('(1)')) return 1;
    if (displayValue.includes('(4)')) return 4;
    // Default to Level 4 for maximum security
    return 4;
  }

  /**
   * Convert authenticator type display value to number
   * @param {string} displayValue
   * @returns {number} Numeric value (0 or 1)
   */
  convertAuthenticatorTypeToNumber(displayValue) {
    if (displayValue.includes('(0)')) return 0;
    if (displayValue.includes('(1)')) return 1;
    // Default to biometric for maximum security
    return 1;
  }

  /**
   * Validate auth level display value
   * @param {string} displayValue
   * @returns {boolean}
   */
  isValidAuthLevel(displayValue) {
    const validValues = this.getAuthLevelOptions().map(option => option.value);
    return validValues.includes(displayValue);
  }

  /**
   * Validate authenticator type display value
   * @param {string} displayValue
   * @returns {boolean}
   */
  isValidAuthenticatorType(displayValue) {
    const validValues = this.getAuthenticatorTypeOptions().map(option => option.value);
    return validValues.includes(displayValue);
  }
}

// Export singleton instance
const DropdownDataService = new DropdownDataServiceClass();
window.DropdownDataService = DropdownDataService;

Error Handling Patterns

Key error handling strategies in the service layer:

  1. Comprehensive Logging: Log all operations with context
  2. Error Transformation: Convert SDK errors to user-friendly messages
  3. State Cleanup: Always attempt to reset state on errors
  4. Non-Blocking Cleanup: Don't let cleanup errors block user flow
  5. Error Categorization: Different handling for different error types

Now let's implement the user interface components for data signing using Cordova's SPA (Single Page Application) architecture with HTML templates and JavaScript modules.

Understanding Cordova SPA Architecture

Cordova applications use a different UI pattern than React Native:

DataSigningInputScreen - Main Form Interface

This is the primary screen where users input data to be signed.

The following image showcases the data signing input screen from the sample application:

Data Signing Input Screen

HTML Template

<!-- www/index.html - Add this template -->

<template id="DataSigningInput-template">
  <div class="screen-container">
    <!-- Header with menu button -->
    <div class="header">
      <button id="data-signing-menu-button" class="menu-button">โ˜ฐ</button>
      <h1 class="title">Data Signing</h1>
      <p class="subtitle">Sign your data with cryptographic authentication</p>
    </div>

    <!-- Info Section -->
    <div class="info-section">
      <div class="info-title">How it works:</div>
      <div class="info-text">
        1. Enter your data payload and select authentication parameters<br>
        2. Click "Sign Data" to initiate the signing process<br>
        3. Complete authentication when prompted<br>
        4. Receive your cryptographically signed data
      </div>
    </div>

    <!-- Error Display -->
    <div id="data-signing-error" class="error-banner" style="display: none;"></div>

    <!-- Form -->
    <div class="form-container">
      
      <!-- Payload Input -->
      <div class="input-group">
        <label class="input-label">Data Payload *</label>
        <textarea
          id="data-signing-payload"
          class="input-field multiline-input"
          placeholder="Enter the data you want to sign..."
          maxlength="500"
          rows="4"
        ></textarea>
        <div class="char-count">
          <span id="data-signing-payload-count">0/500</span>
        </div>
      </div>

      <!-- Auth Level Dropdown -->
      <div class="input-group">
        <label class="input-label">Authentication Level *</label>
        <select id="data-signing-auth-level" class="input-field select-field">
          <option value="">Select Authentication Level</option>
          <!-- Options populated by JavaScript -->
        </select>
        <div class="help-text">Level 4 is recommended for maximum security</div>
      </div>

      <!-- Authenticator Type Dropdown -->
      <div class="input-group">
        <label class="input-label">Authenticator Type *</label>
        <select id="data-signing-authenticator-type" class="input-field select-field">
          <option value="">Select Authenticator Type</option>
          <!-- Options populated by JavaScript -->
        </select>
        <div class="help-text">Choose the authentication method for signing</div>
      </div>

      <!-- Reason Input -->
      <div class="input-group">
        <label class="input-label">Signing Reason *</label>
        <input
          id="data-signing-reason"
          type="text"
          class="input-field"
          placeholder="Enter reason for signing"
          maxlength="100"
        />
        <div class="char-count">
          <span id="data-signing-reason-count">0/100</span>
        </div>
      </div>

    </div>

    <!-- Submit Button -->
    <button id="data-signing-submit-btn" class="primary-button">
      Sign Data
    </button>

  </div>
</template>

JavaScript Module

// www/src/tutorial/screens/dataSigning/DataSigningInputScreen.js

/**
 * Data Signing Input Screen
 * Screen for collecting data signing parameters from user
 *
 * SPA Lifecycle:
 * - onContentLoaded(params) - Called when template loaded
 * - setupEventListeners() - Attach form handlers
 * - registerSDKEventHandlers() - Register data signing response handler
 * - cleanup() - Clean up when navigating away
 */

const DataSigningInputScreen = {
  // Form state
  payload: '',
  selectedAuthLevel: '',
  selectedAuthenticatorType: '',
  reason: '',
  isLoading: false,
  
  // Session info (from params)
  userID: '',
  sessionID: '',
  
  // Original handlers (preservation pattern)
  originalDataSigningHandler: null,

  /**
   * Called when screen content is loaded (SPA lifecycle)
   * @param {Object} params - Navigation parameters
   */
  onContentLoaded(params) {
    console.log('DataSigningInputScreen - Content loaded');

    // Store session info
    this.userID = params.userID || '';
    this.sessionID = params.sessionID || '';

    // Reset form state
    this.payload = '';
    this.selectedAuthLevel = '';
    this.selectedAuthenticatorType = '';
    this.reason = '';
    this.isLoading = false;

    // Setup UI
    this.setupEventListeners();
    this.registerSDKEventHandlers();
    this.populateDropdowns();
    this.clearForm();
    this.hideError();
    this.updateCharCounts();
  },

  /**
   * Setup event listeners for form elements
   */
  setupEventListeners() {
    // Menu button
    const menuButton = document.getElementById('data-signing-menu-button');
    if (menuButton) {
      menuButton.onclick = () => NavigationService.openDrawer();
    }

    // Form submit button
    const submitBtn = document.getElementById('data-signing-submit-btn');
    if (submitBtn) {
      submitBtn.onclick = this.handleSubmit.bind(this);
    }

    // Payload input with character count
    const payloadInput = document.getElementById('data-signing-payload');
    if (payloadInput) {
      payloadInput.oninput = () => {
        this.payload = payloadInput.value;
        this.updateCharCounts();
      };
    }

    // Auth level dropdown
    const authLevelSelect = document.getElementById('data-signing-auth-level');
    if (authLevelSelect) {
      authLevelSelect.onchange = () => {
        this.selectedAuthLevel = authLevelSelect.value;
      };
    }

    // Authenticator type dropdown
    const authenticatorTypeSelect = document.getElementById('data-signing-authenticator-type');
    if (authenticatorTypeSelect) {
      authenticatorTypeSelect.onchange = () => {
        this.selectedAuthenticatorType = authenticatorTypeSelect.value;
      };
    }

    // Reason input with character count
    const reasonInput = document.getElementById('data-signing-reason');
    if (reasonInput) {
      reasonInput.oninput = () => {
        this.reason = reasonInput.value;
        this.updateCharCounts();
      };
    }
  },

  /**
   * Register SDK event handlers for data signing
   */
  registerSDKEventHandlers() {
    const eventManager = rdnaService.getEventManager();

    // Preserve original handler
    this.originalDataSigningHandler = eventManager.dataSigningResponseHandler;

    // Register data signing response handler
    eventManager.setDataSigningResponseHandler((data) => {
      this.handleDataSigningResponse(data);
    });
  },

  /**
   * Populate dropdowns with options
   */
  populateDropdowns() {
    // Populate auth level dropdown
    const authLevelSelect = document.getElementById('data-signing-auth-level');
    if (authLevelSelect && DropdownDataService) {
      const options = DropdownDataService.getAuthLevelOptions();
      authLevelSelect.innerHTML = '<option value="">Select Authentication Level</option>';
      options.forEach(option => {
        const optionElement = document.createElement('option');
        optionElement.value = option.value;
        optionElement.textContent = \`\${option.value} - \${option.description}\`;
        authLevelSelect.appendChild(optionElement);
      });
    }

    // Populate authenticator type dropdown
    const authenticatorTypeSelect = document.getElementById('data-signing-authenticator-type');
    if (authenticatorTypeSelect && DropdownDataService) {
      const options = DropdownDataService.getAuthenticatorTypeOptions();
      authenticatorTypeSelect.innerHTML = '<option value="">Select Authenticator Type</option>';
      options.forEach(option => {
        const optionElement = document.createElement('option');
        optionElement.value = option.value;
        optionElement.textContent = \`\${option.value} - \${option.description}\`;
        authenticatorTypeSelect.appendChild(optionElement);
      });
    }
  },

  /**
   * Handle form submission
   */
  async handleSubmit() {
    console.log('DataSigningInputScreen - Submit button clicked');

    // Validate form
    const validation = DataSigningService.validateSigningInput(
      this.payload,
      this.selectedAuthLevel,
      this.selectedAuthenticatorType,
      this.reason
    );

    if (!validation.isValid) {
      this.showError(validation.errors.join(', '));
      return;
    }

    // Hide error and show loading
    this.hideError();
    this.setLoading(true);

    try {
      // Set context for password authentication
      DataSigningSetupAuthManager.setContext({
        payload: this.payload,
        authLevel: DropdownDataService.convertAuthLevelToNumber(this.selectedAuthLevel),
        authenticatorType: DropdownDataService.convertAuthenticatorTypeToNumber(this.selectedAuthenticatorType),
        reason: this.reason
      });

      // Call DataSigningService
      await DataSigningService.signData(
        this.payload,
        this.selectedAuthLevel,
        this.selectedAuthenticatorType,
        this.reason
      );

      console.log('DataSigningInputScreen - SignData API call successful');
      // Result will come via onAuthenticateUserAndSignData event

    } catch (error) {
      console.error('DataSigningInputScreen - Submit error:', error);
      this.setLoading(false);
      this.showError(error?.error?.errorString || 'Failed to submit request');
      DataSigningSetupAuthManager.clearContext();
    }
  },

  /**
   * Handle data signing response event
   */
  handleDataSigningResponse(data) {
    console.log('DataSigningInputScreen - Processing data signing response');

    this.setLoading(false);

    // Hide password modal
    if (window.PasswordChallengeModal) {
      PasswordChallengeModal.hide();
    }

    // Check for errors
    if (data.error && data.error.longErrorCode !== 0) {
      this.showError(DataSigningService.getErrorMessage(data.error.longErrorCode));
      DataSigningSetupAuthManager.handleError(data.error);
      return;
    }

    // Check status code
    if (data.status && data.status.statusCode !== 100) {
      this.showError(\`Signing failed: \${data.status.statusMessage}\`);
      DataSigningSetupAuthManager.handleError(data);
      return;
    }

    // Success! Navigate to result screen
    console.log('DataSigningInputScreen - Data signing successful');
    DataSigningSetupAuthManager.handleSuccess(data);

    NavigationService.navigate('DataSigningResult', {
      resultData: data
    });
  },

  /**
   * Update character count displays
   */
  updateCharCounts() {
    const payloadCount = document.getElementById('data-signing-payload-count');
    const reasonCount = document.getElementById('data-signing-reason-count');

    if (payloadCount) {
      payloadCount.textContent = \`\${this.payload.length}/500\`;
    }

    if (reasonCount) {
      reasonCount.textContent = \`\${this.reason.length}/100\`;
    }
  },

  /**
   * Show error message
   */
  showError(message) {
    const errorDiv = document.getElementById('data-signing-error');
    if (errorDiv) {
      errorDiv.textContent = message;
      errorDiv.style.display = 'block';
    }
  },

  /**
   * Hide error message
   */
  hideError() {
    const errorDiv = document.getElementById('data-signing-error');
    if (errorDiv) {
      errorDiv.style.display = 'none';
    }
  },

  /**
   * Set loading state
   */
  setLoading(loading) {
    this.isLoading = loading;

    const submitBtn = document.getElementById('data-signing-submit-btn');
    if (submitBtn) {
      submitBtn.disabled = loading;
      submitBtn.textContent = loading ? 'Processing...' : 'Sign Data';
    }
  },

  /**
   * Clear form fields
   */
  clearForm() {
    const payloadInput = document.getElementById('data-signing-payload');
    const authLevelSelect = document.getElementById('data-signing-auth-level');
    const authenticatorTypeSelect = document.getElementById('data-signing-authenticator-type');
    const reasonInput = document.getElementById('data-signing-reason');

    if (payloadInput) payloadInput.value = '';
    if (authLevelSelect) authLevelSelect.value = '';
    if (authenticatorTypeSelect) authenticatorTypeSelect.value = '';
    if (reasonInput) reasonInput.value = '';

    this.updateCharCounts();
  },

  /**
   * Cleanup when navigating away
   */
  cleanup() {
    const eventManager = rdnaService.getEventManager();
    if (this.originalDataSigningHandler) {
      eventManager.setDataSigningResponseHandler(this.originalDataSigningHandler);
    }
  }
};

// Make available globally
window.DataSigningInputScreen = DataSigningInputScreen;

DataSigningResultScreen - Results Display

The following image showcases the successful data signing results screen:

Data Signing Success Results

HTML Template

<!-- www/index.html - Add this template -->

<template id="DataSigningResult-template">
  <div class="screen-container">
    <!-- Success Header -->
    <div class="success-header">
      <div class="success-icon">โœ…</div>
      <h1 class="success-title">Data Signing Successful!</h1>
      <p class="success-subtitle">Your data has been cryptographically signed</p>
    </div>

    <!-- Results Section -->
    <div class="results-section">
      <h2 class="section-title">Signing Results</h2>
      <p class="section-subtitle">All values below have been cryptographically verified</p>

      <!-- Results Container (populated by JavaScript) -->
      <div id="data-signing-results-container"></div>
    </div>

    <!-- Actions Section -->
    <div class="actions-section">
      <button id="data-signing-sign-another-btn" class="primary-button">
        ๐Ÿ” Sign Another Document
      </button>
    </div>
  </div>
</template>

JavaScript Module

// www/src/tutorial/screens/dataSigning/DataSigningResultScreen.js

const DataSigningResultScreen = {
  resultData: null,
  resultDisplay: null,
  copiedField: null,

  /**
   * Called when screen content is loaded
   */
  onContentLoaded(params) {
    console.log('DataSigningResultScreen - Content loaded');

    this.resultData = params?.resultData;

    if (!this.resultData) {
      this.showError('No signing results available');
      return;
    }

    // Format for display
    this.resultDisplay = DataSigningService.formatSigningResultForDisplay(this.resultData);

    // Setup UI
    this.setupEventListeners();
    this.renderResults();
  },

  /**
   * Setup event listeners
   */
  setupEventListeners() {
    const signAnotherBtn = document.getElementById('data-signing-sign-another-btn');
    if (signAnotherBtn) {
      signAnotherBtn.onclick = this.handleSignAnother.bind(this);
    }
  },

  /**
   * Render results dynamically
   */
  renderResults() {
    const resultsContainer = document.getElementById('data-signing-results-container');
    if (!resultsContainer) return;

    const resultItems = DataSigningService.convertToResultInfoItems(this.resultDisplay);

    let html = '';
    resultItems.forEach((item) => {
      const isSignature = item.name === 'Payload Signature';
      html += \`
        <div class="result-card \${isSignature ? 'signature-card' : ''}">
          <div class="result-card-header">
            <span class="result-card-label">\${item.name}</span>
            \${item.value !== 'N/A' ? \`
              <button class="copy-btn" data-field="\${item.name}" data-value="\${this.escapeHtml(item.value)}">
                ๐Ÿ“‹ Copy
              </button>
            \` : ''}
          </div>
          <div class="result-card-value">
            \${this.escapeHtml(item.value)}
          </div>
        </div>
      \`;
    });

    resultsContainer.innerHTML = html;

    // Attach copy handlers
    const copyBtns = resultsContainer.querySelectorAll('.copy-btn');
    copyBtns.forEach(btn => {
      btn.onclick = () => {
        const fieldName = btn.getAttribute('data-field');
        const value = btn.getAttribute('data-value');
        this.handleCopyToClipboard(value, fieldName, btn);
      };
    });
  },

  /**
   * Handle copy to clipboard
   */
  async handleCopyToClipboard(value, fieldName, button) {
    try {
      // Modern Clipboard API
      if (navigator.clipboard && navigator.clipboard.writeText) {
        await navigator.clipboard.writeText(value);
      } else {
        // Fallback
        const textArea = document.createElement('textarea');
        textArea.value = value;
        textArea.style.position = 'fixed';
        textArea.style.left = '-9999px';
        document.body.appendChild(textArea);
        textArea.select();
        document.execCommand('copy');
        document.body.removeChild(textArea);
      }

      // Show success
      button.textContent = 'โœ“ Copied';
      button.classList.add('copied');

      setTimeout(() => {
        button.textContent = '๐Ÿ“‹ Copy';
        button.classList.remove('copied');
      }, 2000);

    } catch (error) {
      console.error('Copy failed:', error);
      alert('Failed to copy. Please copy manually.');
    }
  },

  /**
   * Handle "Sign Another Document"
   */
  async handleSignAnother() {
    try {
      await DataSigningService.resetState();
      NavigationService.navigate('DataSigningInput', {
        userID: this.userID,
        sessionID: this.sessionID
      });
    } catch (error) {
      NavigationService.navigate('DataSigningInput');
    }
  },

  /**
   * Show error
   */
  showError(message) {
    const container = document.getElementById('data-signing-results-container');
    if (container) {
      container.innerHTML = \`
        <div class="error-container">
          <div class="error-icon">โš ๏ธ</div>
          <div class="error-message">\${this.escapeHtml(message)}</div>
          <button class="back-btn" onclick="NavigationService.navigate('DataSigningInput')">
            Back to Data Signing
          </button>
        </div>
      \`;
    }
  },

  /**
   * Escape HTML
   */
  escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
};

// Make available globally
window.DataSigningResultScreen = DataSigningResultScreen;

Key UI Features Implemented

  1. ๐Ÿ“ Form Validation: Real-time validation with character counters
  2. ๐ŸŽ›๏ธ Dropdown Components: Native HTML select elements with dynamic options
  3. โณ Loading States: Proper loading indicators during operations
  4. ๐Ÿ“‹ Copy Functionality: One-click copying of signature data with modern Clipboard API
  5. ๐Ÿ“ฑ Responsive Design: Clean, modern UI using standard CSS
  6. ๐Ÿ”„ Navigation Flow: SPA navigation with template cloning
  7. ๐Ÿ›ก๏ธ Error Handling: User-friendly error messages and recovery

PasswordChallengeModal - Step-Up Authentication Component

When data signing requires password verification (challengeMode 12), a modal dialog prompts the user for authentication.

The following image showcases the password challenge modal during data signing:

Password Challenge Modal

Modal HTML Structure

<!-- www/index.html - Add this modal div (not a template, persistent in DOM) -->

<div id="data-signing-password-modal" class="modal-overlay" style="display: none;">
  <div class="modal-container">
    <!-- Modal content populated by JavaScript -->
    <div id="data-signing-password-modal-content"></div>
  </div>
</div>

Modal CSS Styling

/* www/css/style.css - Add modal styles */

.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}

.modal-container {
  background: white;
  border-radius: 12px;
  width: 90%;
  max-width: 400px;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}

.password-modal-header {
  padding: 20px;
  border-bottom: 1px solid #eee;
}

.password-modal-title {
  font-size: 18px;
  font-weight: 600;
  color: #1a1a1a;
  margin: 0;
}

.password-modal-body {
  padding: 20px;
}

.password-modal-context {
  background-color: #f8f9fa;
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 16px;
}

.password-modal-context-label {
  font-size: 12px;
  color: #666;
  margin-bottom: 4px;
}

.password-modal-context-value {
  font-size: 14px;
  color: #1a1a1a;
  font-weight: 500;
}

.password-modal-attempts {
  background-color: #e8f4fd;
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 16px;
  text-align: center;
}

.password-modal-attempts.warning {
  background-color: #fff3cd;
}

.password-modal-attempts.danger {
  background-color: #f8d7da;
}

.password-modal-attempts-text {
  font-size: 14px;
  font-weight: 500;
  color: #1a1a1a;
}

.password-modal-error {
  background-color: #f8d7da;
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 16px;
}

.password-modal-error-text {
  font-size: 14px;
  color: #721c24;
}

.password-modal-input-group {
  margin-bottom: 16px;
}

.password-modal-label {
  display: block;
  font-size: 14px;
  font-weight: 500;
  color: #1a1a1a;
  margin-bottom: 8px;
}

.password-modal-input-wrapper {
  position: relative;
}

.password-modal-input {
  width: 100%;
  padding: 12px;
  padding-right: 40px;
  border: 1px solid #ddd;
  border-radius: 8px;
  font-size: 16px;
  box-sizing: border-box;
}

.password-modal-input:focus {
  outline: none;
  border-color: #007AFF;
}

.password-modal-toggle-btn {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  padding: 8px;
  cursor: pointer;
  font-size: 18px;
}

.password-modal-footer {
  padding: 20px;
  border-top: 1px solid #eee;
  display: flex;
  gap: 12px;
}

.password-modal-btn {
  flex: 1;
  padding: 12px 24px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: opacity 0.2s;
}

.password-modal-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.password-modal-btn-primary {
  background-color: #007AFF;
  color: white;
}

.password-modal-btn-secondary {
  background-color: #f0f0f0;
  color: #1a1a1a;
}

JavaScript Module

// www/src/tutorial/screens/dataSigning/PasswordChallengeModal.js

/**
 * Password Challenge Modal for Data Signing
 * 
 * Shows modal when SDK triggers getPassword event with challengeMode 12
 * during data signing authentication flow.
 * 
 * Features:
 * - Password input with visibility toggle
 * - Attempts counter with color coding (green โ†’ orange โ†’ red)
 * - Error display from SDK responses
 * - Submit and Cancel actions
 * - Auto-focus on password input
 * - Keyboard handling (Enter to submit)
 */

const PasswordChallengeModal = {
  // State
  visible: false,
  challengeMode: 12,
  attemptsLeft: 3,
  errorMessage: '',
  isSubmitting: false,
  passwordVisible: false,

  // Context (what's being signed)
  context: {
    payload: '',
    reason: ''
  },

  /**
   * Show modal with configuration
   * @param {number} challengeMode - Challenge mode (should be 12 for data signing)
   * @param {number} attemptsLeft - Remaining authentication attempts
   * @param {Object} context - Signing context (payload, reason)
   */
  show(challengeMode, attemptsLeft, context) {
    console.log('PasswordChallengeModal - Showing modal:', {
      challengeMode,
      attemptsLeft,
      hasContext: !!context
    });

    this.visible = true;
    this.challengeMode = challengeMode;
    this.attemptsLeft = attemptsLeft;
    this.context = context || {};
    this.errorMessage = '';
    this.isSubmitting = false;
    this.passwordVisible = false;

    // Render and show
    this.render();
    
    const modalElement = document.getElementById('data-signing-password-modal');
    if (modalElement) {
      modalElement.style.display = 'flex';
    }

    // Auto-focus password input after modal renders
    setTimeout(() => {
      const passwordInput = document.getElementById('password-modal-input');
      if (passwordInput) {
        passwordInput.focus();
      }
    }, 300);
  },

  /**
   * Update modal state (for re-triggered getPassword events)
   * @param {Object} updates - State updates
   */
  update(updates) {
    console.log('PasswordChallengeModal - Updating modal:', updates);

    if (updates.attemptsLeft !== undefined) {
      this.attemptsLeft = updates.attemptsLeft;
    }
    if (updates.errorMessage !== undefined) {
      this.errorMessage = updates.errorMessage;
    }

    // Re-render with updated state
    this.render();

    // Clear and refocus password input
    const passwordInput = document.getElementById('password-modal-input');
    if (passwordInput) {
      passwordInput.value = '';
      passwordInput.focus();
    }
  },

  /**
   * Hide modal and cleanup
   */
  hide() {
    console.log('PasswordChallengeModal - Hiding modal');

    this.visible = false;
    this.errorMessage = '';
    this.isSubmitting = false;
    this.passwordVisible = false;

    const modalElement = document.getElementById('data-signing-password-modal');
    if (modalElement) {
      modalElement.style.display = 'none';
    }

    // Clear password input
    const passwordInput = document.getElementById('password-modal-input');
    if (passwordInput) {
      passwordInput.value = '';
    }
  },

  /**
   * Render modal HTML dynamically
   */
  render() {
    const contentElement = document.getElementById('data-signing-password-modal-content');
    if (!contentElement) return;

    // Determine attempts color
    const attemptsClass = this.getAttemptsClass();
    const attemptsColor = this.getAttemptsColor();

    contentElement.innerHTML = \`
      <div class="password-modal-header">
        <h2 class="password-modal-title">๐Ÿ” Authentication Required</h2>
      </div>

      <div class="password-modal-body">
        
        <!-- Context: What's being signed -->
        \${this.context.reason ? \`
          <div class="password-modal-context">
            <div class="password-modal-context-label">Signing Reason:</div>
            <div class="password-modal-context-value">\${this.escapeHtml(this.context.reason)}</div>
          </div>
        \` : ''}

        <!-- Attempts Counter -->
        \${this.attemptsLeft <= 3 ? \`
          <div class="password-modal-attempts \${attemptsClass}">
            <div class="password-modal-attempts-text" style="color: \${attemptsColor};">
              \${this.attemptsLeft} attempt\${this.attemptsLeft !== 1 ? 's' : ''} remaining
            </div>
          </div>
        \` : ''}

        <!-- Error Display -->
        \${this.errorMessage ? \`
          <div class="password-modal-error">
            <div class="password-modal-error-text">\${this.escapeHtml(this.errorMessage)}</div>
          </div>
        \` : ''}

        <!-- Password Input -->
        <div class="password-modal-input-group">
          <label class="password-modal-label">Password</label>
          <div class="password-modal-input-wrapper">
            <input
              type="\${this.passwordVisible ? 'text' : 'password'}"
              id="password-modal-input"
              class="password-modal-input"
              placeholder="Enter your password"
              \${this.isSubmitting ? 'disabled' : ''}
            />
            <button
              id="password-modal-toggle-btn"
              class="password-modal-toggle-btn"
              type="button"
              \${this.isSubmitting ? 'disabled' : ''}
            >
              \${this.passwordVisible ? '๐Ÿ‘๏ธ' : '๐Ÿ™ˆ'}
            </button>
          </div>
        </div>

      </div>

      <div class="password-modal-footer">
        <button
          id="password-modal-cancel-btn"
          class="password-modal-btn password-modal-btn-secondary"
          \${this.isSubmitting ? 'disabled' : ''}
        >
          Cancel
        </button>
        <button
          id="password-modal-submit-btn"
          class="password-modal-btn password-modal-btn-primary"
          \${this.isSubmitting ? 'disabled' : ''}
        >
          \${this.isSubmitting ? 'Verifying...' : 'Verify & Continue'}
        </button>
      </div>
    \`;

    // Attach event listeners
    this.attachEventListeners();
  },

  /**
   * Attach event listeners to modal elements
   */
  attachEventListeners() {
    // Submit button
    const submitBtn = document.getElementById('password-modal-submit-btn');
    if (submitBtn) {
      submitBtn.onclick = () => this.handleSubmit();
    }

    // Cancel button
    const cancelBtn = document.getElementById('password-modal-cancel-btn');
    if (cancelBtn) {
      cancelBtn.onclick = () => this.handleCancel();
    }

    // Toggle visibility button
    const toggleBtn = document.getElementById('password-modal-toggle-btn');
    if (toggleBtn) {
      toggleBtn.onclick = () => this.togglePasswordVisibility();
    }

    // Password input - Enter key to submit
    const passwordInput = document.getElementById('password-modal-input');
    if (passwordInput) {
      passwordInput.onkeypress = (e) => {
        if (e.key === 'Enter' && !this.isSubmitting) {
          this.handleSubmit();
        }
      };
    }
  },

  /**
   * Handle submit button click
   */
  handleSubmit() {
    if (this.isSubmitting) return;

    const passwordInput = document.getElementById('password-modal-input');
    const password = passwordInput?.value.trim();

    if (!password) {
      this.errorMessage = 'Please enter your password';
      this.render();
      return;
    }

    console.log('PasswordChallengeModal - Submitting password');
    this.isSubmitting = true;
    this.render();

    // Call DataSigningService to submit password
    DataSigningService.submitPassword(password, this.challengeMode)
      .then(() => {
        console.log('PasswordChallengeModal - Password submitted successfully');
        // Keep modal visible - SDK will trigger response event
      })
      .catch((error) => {
        console.error('PasswordChallengeModal - Password submission failed:', error);
        this.isSubmitting = false;
        this.errorMessage = error?.error?.errorString || 'Authentication failed';
        this.render();
      });
  },

  /**
   * Handle cancel button click
   */
  handleCancel() {
    console.log('PasswordChallengeModal - Cancel clicked');

    // Call DataSigningService to reset state
    DataSigningService.resetState()
      .then(() => {
        this.hide();
      })
      .catch((error) => {
        console.error('PasswordChallengeModal - Reset failed:', error);
        this.hide(); // Hide anyway
      });
  },

  /**
   * Toggle password visibility
   */
  togglePasswordVisibility() {
    this.passwordVisible = !this.passwordVisible;
    this.render();
    
    // Maintain focus
    const passwordInput = document.getElementById('password-modal-input');
    if (passwordInput) {
      passwordInput.focus();
    }
  },

  /**
   * Get attempts CSS class based on count
   * @returns {string} CSS class
   */
  getAttemptsClass() {
    if (this.attemptsLeft === 1) return 'danger';
    if (this.attemptsLeft === 2) return 'warning';
    return '';
  },

  /**
   * Get attempts color based on count
   * @returns {string} Color code
   */
  getAttemptsColor() {
    if (this.attemptsLeft === 1) return '#dc2626'; // Red
    if (this.attemptsLeft === 2) return '#f59e0b'; // Orange
    return '#10b981'; // Green
  },

  /**
   * Escape HTML to prevent XSS
   * @param {string} text - Text to escape
   * @returns {string} Escaped text
   */
  escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
};

// Make available globally
if (typeof window !== 'undefined') {
  window.PasswordChallengeModal = PasswordChallengeModal;
}

DataSigningSetupAuthManager Integration

The modal is controlled by the DataSigningSetupAuthManager, which handles challengeMode 12 events:

// www/src/uniken/managers/DataSigningSetupAuthManager.js

/**
 * Manager for handling data signing authentication context and password challenges
 * 
 * Responsibilities:
 * - Store data signing context (payload, auth level, etc.)
 * - Show/hide password challenge modal
 * - Handle password submission with challengeMode 12
 * - Handle cancellation and cleanup
 */

const DataSigningSetupAuthManager = {
  // Context storage
  context: null,

  /**
   * Set signing context (called before initiating signing)
   * @param {Object} context - Signing parameters
   */
  setContext(context) {
    console.log('DataSigningSetupAuthManager - Setting context');
    this.context = {
      payload: context.payload,
      authLevel: context.authLevel,
      authenticatorType: context.authenticatorType,
      reason: context.reason,
      userID: context.userID || ''
    };
  },

  /**
   * Get stored context
   * @returns {Object|null}
   */
  getContext() {
    return this.context;
  },

  /**
   * Clear stored context
   */
  clearContext() {
    console.log('DataSigningSetupAuthManager - Clearing context');
    this.context = null;
  },

  /**
   * Check if manager has active context
   * @returns {boolean}
   */
  isActive() {
    return this.context !== null;
  },

  /**
   * Show password challenge modal
   * Called when SDK triggers getPassword with challengeMode 12
   * @param {Object} data - getPassword event data
   */
  showPasswordDialog(data) {
    console.log('DataSigningSetupAuthManager - Showing password dialog');

    // Check if this is first time or re-trigger
    if (PasswordChallengeModal.visible) {
      // Re-trigger: Update with new error/attempts
      PasswordChallengeModal.update({
        attemptsLeft: data.attemptsLeft,
        errorMessage: data.challengeResponse?.status?.statusMessage || ''
      });
    } else {
      // First time: Show modal
      PasswordChallengeModal.show(
        data.challengeMode,
        data.attemptsLeft,
        {
          payload: this.context?.payload,
          reason: this.context?.reason
        }
      );
    }
  },

  /**
   * Hide password dialog
   */
  hidePasswordDialog() {
    PasswordChallengeModal.hide();
  },

  /**
   * Handle successful signing (called from DataSigningInputScreen)
   * @param {Object} signingResult - Signing response data
   */
  handleSuccess(signingResult) {
    console.log('DataSigningSetupAuthManager - Handling success');
    this.clearContext();
    this.hidePasswordDialog();
  },

  /**
   * Handle signing error (called from DataSigningInputScreen)
   * @param {Object} error - Error data
   */
  handleError(error) {
    console.log('DataSigningSetupAuthManager - Handling error');
    this.clearContext();
    this.hidePasswordDialog();
  }
};

// Make available globally
if (typeof window !== 'undefined') {
  window.DataSigningSetupAuthManager = DataSigningSetupAuthManager;
}

SDKEventProvider Integration

The SDKEventProvider routes challengeMode 12 events to the manager:

// www/src/uniken/providers/SDKEventProvider.js (Data Signing Section)

/**
 * Handle getPassword event - route based on challengeMode
 */
handleGetPassword(data) {
  console.log('SDKEventProvider - getPassword event, challengeMode:', data.challengeMode);

  // ... other challengeMode handling (0, 1, 2, 3, 4, etc.) ...

  // challengeMode 12: Data signing password verification
  else if (data.challengeMode === 12) {
    if (typeof DataSigningSetupAuthManager !== 'undefined' && DataSigningSetupAuthManager.isActive()) {
      DataSigningSetupAuthManager.showPasswordDialog(data);
    } else {
      console.warn('SDKEventProvider - DataSigningSetupAuthManager not active for challengeMode 12');
    }
  }
}

Key Features

  1. ๐Ÿ“ฑ Context Display: Shows what's being signed (reason)
  2. ๐Ÿ”ข Attempts Counter: Color-coded (green โ†’ orange โ†’ red)
  3. ๐Ÿ‘๏ธ Password Toggle: Show/hide password visibility
  4. โŒจ๏ธ Keyboard Support: Enter key to submit
  5. ๐Ÿ›ก๏ธ Error Display: Shows SDK error messages
  6. โณ Loading State: "Verifying..." during submission
  7. ๐Ÿ”„ Re-trigger Support: Updates on wrong password without closing
  8. ๐Ÿงน Auto Cleanup: Clears password immediately after use

Modal Flow

1. User submits signing form
   โ†“
2. SDK determines authentication needed
   โ†“
3. SDK triggers getPassword with challengeMode 12
   โ†“
4. SDKEventProvider routes to DataSigningSetupAuthManager
   โ†“
5. Manager shows PasswordChallengeModal
   โ†“
6. User enters password and submits
   โ†“
7. DataSigningService.submitPassword() called
   โ†“
8a. SUCCESS: SDK triggers onAuthenticateUserAndSignData
    โ†’ Modal stays visible until response
    โ†’ DataSigningInputScreen navigates to results
    โ†’ Modal hidden
    
8b. WRONG PASSWORD: SDK re-triggers getPassword
    โ†’ Modal updates with error and decremented attempts
    โ†’ User can retry
    
8c. CANCEL: User clicks Cancel button
    โ†’ DataSigningService.resetState() called
    โ†’ Modal hidden
    โ†’ Form returns to editable state

Let's explore comprehensive testing strategies for your data signing implementation.

Testing Scenarios Overview

Test these key scenarios to ensure complete functionality:

  1. Happy Path Flow: Complete successful signing
  2. Error Scenarios: Network failures, auth errors
  3. State Cleanup: Reset on cancellation
  4. Security Validation: Auth level enforcement
  5. UI Responsiveness: Loading states and feedback

Test Case 1: Successful Data Signing

Manual Testing Steps:

  1. Input Validation
    • Open Data Signing screen from dashboard
    • Enter payload: "Test transaction #12345"
    • Select auth level: "RDNA_AUTH_LEVEL_4 (4)"
    • Select authenticator: "RDNA_IDV_SERVER_BIOMETRIC (1)"
    • Enter reason: "Testing data signing flow"
    • Verify character counts update: 25/500, 27/100
  2. Form Submission
    • Click "Sign Data" button
    • Verify button changes to "Processing..."
    • Verify form fields disabled during loading
  3. Password Challenge (if triggered)
    • Password modal appears with authentication options
    • Enter password
    • Verify attempts counter displays correctly
    • Submit authentication
  4. Final Result
    • Result screen displays with success message
    • All 7 result fields populated:
      • Payload Signature (base64 string)
      • Data Signature ID (UUID)
      • Reason, Data Payload, Auth Level, Auth Type, Length
    • Copy functionality works for all fields
    • "Sign Another Document" button navigates back
  5. State Cleanup
    • Form cleared on return to input screen
    • No residual data from previous signing

Console Output Pattern:

DataSigningInputScreen - Submit button clicked
DataSigningService - Starting data signing process
RdnaService - Initiating data signing...
RdnaService - AuthenticateUserAndSignData sync response success
[Password challenge if required]
RdnaEventManager - onAuthenticateUserAndSignData event received
DataSigningInputScreen - Data signing successful

Test Case 2: Authentication Failure

Error Scenarios to Test:

// Error Code 102: Authentication failed
// Expected: Error message displayed, retry available

// Error Code 214: Authentication method not supported
// Expected: User-friendly message, suggest alternative method

// Error Code 153: Operation cancelled by user
// Expected: Clean state reset, return to input form

Testing Steps:

  1. Initiate signing with Level 4 + Biometric
  2. Cancel biometric prompt (if device supports)
  3. Verify error message: "Operation cancelled by user"
  4. Verify form is still editable for retry
  5. Verify no ghost state remains

Test Case 3: Network/Connection Errors

Testing Steps:

  1. Disable network connection
  2. Attempt data signing
  3. Verify timeout handling
  4. Verify user-friendly error message
  5. Re-enable network
  6. Verify retry works correctly

Test Case 4: Cancel Flow Validation

Scenarios:

  1. Password Modal Cancellation
    • Trigger password authentication
    • Click "Cancel" in password modal
    • Verify resetAuthenticateUserAndSignDataState() called
    • Verify form state reset
    • Verify modal hidden
  2. Navigation Cancellation
    • Start signing process
    • Navigate away (open drawer, go to dashboard)
    • Verify state cleanup on screen change
    • Return to data signing
    • Verify clean form state
  3. Multiple Reset Calls
    • Verify reset is idempotent (safe to call multiple times)
    • No errors on duplicate cleanup calls

Test Case 5: Input Validation

Test all validation rules:

// Required field validation
Payload: "" โ†’ "Payload is required"
Auth Level: "" โ†’ "Please select an authentication level"
Authenticator Type: "" โ†’ "Please select an authenticator type"
Reason: "" โ†’ "Reason is required"

// Character limit enforcement
Payload: 501 chars โ†’ "Payload must be less than 500 characters"
Reason: 101 chars โ†’ "Reason must be less than 100 characters"

// Dropdown validation
Invalid auth level โ†’ "Invalid authentication level"
Invalid authenticator โ†’ "Invalid authenticator type"

Test Case 6: Authentication Level Enforcement

Test unsupported levels:

// Levels 2 and 3 are NOT SUPPORTED
// Verify these options don't appear in dropdown
// If manually set, SDK should return error

// Test supported combinations:
Level 0 + Type 0 โ†’ Should work (testing only)
Level 1 + Type 0 โ†’ Should work (standard signing)
Level 4 + Type 1 โ†’ Should work (max security)

// Test invalid combinations:
Level 4 + Type 0 โ†’ Should fail with error 214
Level 4 + Type 2 โ†’ Should fail (not in dropdown)

Test Case 7: Loading States & UI Feedback

Verify all loading states:

  1. Form Submission Loading
    • Button text: "Sign Data" โ†’ "Processing..."
    • Button disabled
    • Form inputs disabled
    • Character counters still visible
  2. Copy Feedback
    • Copy button: "๐Ÿ“‹ Copy" โ†’ "โœ“ Copied"
    • Green highlight for 2 seconds
    • Revert to original state
  3. Error Display
    • Error banner appears at top of form
    • Red background with error icon
    • Error message clearly readable
    • Dismisses on form edit or retry

Validation Checklist

Use this checklist to verify complete implementation:

Core Functionality:

Error Handling:

Security Validation:

UI/UX Quality:

Debugging Console Commands

Use these browser console commands for debugging:

// Check current screen
NavigationService.getCurrentRoute()

// Inspect form state
DataSigningInputScreen.payload
DataSigningInputScreen.selectedAuthLevel
DataSigningInputScreen.isLoading

// Test dropdown service
DropdownDataService.getAuthLevelOptions()
DropdownDataService.convertAuthLevelToNumber("RDNA_AUTH_LEVEL_4 (4)")

// Test validation
DataSigningService.validateSigningInput("test", "RDNA_AUTH_LEVEL_4 (4)", "RDNA_IDV_SERVER_BIOMETRIC (1)", "test reason")

// Manually trigger events
rdnaService.getEventManager().dataSigningResponseHandler({...testData})

Cordova-Specific Issues

"Can't find variable: com"

"Plugin not found"

Changes Not Reflecting

Debugging Tools

Browser DevTools:

Useful Console Commands:

// Check plugin availability
typeof com !== 'undefined'

// Check specific method
typeof com.uniken.rdnaplugin.RdnaClient.authenticateUserAndSignData

// Inspect event manager state
rdnaService.getEventManager()

// Check screen state
DataSigningInputScreen.isLoading
DataSigningInputScreen.payload

// Test service methods
DataSigningService.getErrorMessage(214)

Logging Best Practices:

// Use structured logging for debugging
console.log('DataSigningInputScreen - Submit:', JSON.stringify({
  payloadLength: payload.length,
  authLevel,
  authenticatorType,
  reasonLength: reason.length
}, null, 2));

// Log all SDK responses
console.log('SDK Response:', JSON.stringify(result, null, 2));

Authentication Level Selection Guidelines

Choose the appropriate authentication level based on data sensitivity:

Data Sensitivity

Level

Use Cases

Security Features

Testing/Dev

0 (NONE)

Development, testing

โš ๏ธ No authentication

Standard

1

Public documents, low-risk

Device biometric/passcode/password

High Security

4

Financial, legal, medical, compliance

Server biometric verification

Implementation Guidelines:

// โœ… GOOD: Use switch-based selection with clear use cases
function selectAuthLevel(documentType) {
  switch (documentType) {
    case 'financial':
    case 'legal':
    case 'medical':
      // Use maximum security for sensitive data
      return {
        authLevel: 4,  // RDNA_AUTH_LEVEL_4
        authenticatorType: 1  // RDNA_IDV_SERVER_BIOMETRIC
      };
      
    case 'internal':
    case 'approval':
      // Use standard security for routine operations
      return {
        authLevel: 1,  // RDNA_AUTH_LEVEL_1
        authenticatorType: 0  // NONE (auto-select)
      };
      
    case 'testing':
      // Only for development environments
      if (process.env.NODE_ENV !== 'production') {
        return {
          authLevel: 0,  // NONE
          authenticatorType: 0
        };
      }
      throw new Error('Testing mode not allowed in production');
      
    default:
      // Default to maximum security
      return {
        authLevel: 4,
        authenticatorType: 1
      };
  }
}

// โŒ BAD: Using unsupported levels
const config = {
  authLevel: 2,  // NOT SUPPORTED - will cause SDK error
  authenticatorType: 0
};

// โŒ BAD: Using wrong authenticator type with Level 4
const config = {
  authLevel: 4,
  authenticatorType: 0  // Must be 1 (RDNA_IDV_SERVER_BIOMETRIC)
};

State Management Security Patterns

Proper State Cleanup:

// โœ… GOOD: Complete cleanup sequence
async function cleanupDataSigning() {
  try {
    // 1. Reset SDK authentication state
    await DataSigningService.resetState();
    
    // 2. Clear sensitive form data
    DataSigningInputScreen.payload = '';
    DataSigningInputScreen.reason = '';
    
    // 3. Reset authentication modal state
    PasswordChallengeModal.hide();
    
    // 4. Clear results (optional - depends on requirements)
    DataSigningResultScreen.resultData = null;
    
    console.log('Data signing state cleaned successfully');
  } catch (error) {
    console.error('Cleanup failed:', error);
    // Continue - cleanup failure should not block user
  }
}

// โŒ BAD: Incomplete cleanup leaves sensitive data in memory
function badCleanup() {
  // Only clears UI, SDK state remains
  document.getElementById('data-signing-payload').value = '';
}

Memory Management for Sensitive Data:

// โœ… GOOD: Clear password immediately after use
async function submitPassword(password, challengeMode) {
  try {
    const response = await rdnaService.setPassword(password, challengeMode);
    
    // Clear password from memory immediately
    password = '';
    
    // Clear from modal state
    PasswordChallengeModal.password = '';
    
    return response;
  } catch (error) {
    // Still clear password on error
    password = '';
    throw error;
  }
}

// โŒ BAD: Password persists in memory
const modalState = {
  password: 'user-password',  // Stored indefinitely
  isVisible: true
};

Error Handling Security Patterns

Secure Error Message Display:

// โœ… GOOD: Map error codes to user-friendly messages
function getErrorMessage(errorCode) {
  const errorMessages = {
    0: 'Success',
    102: 'Authentication failed. Please check your credentials and try again.',
    153: 'Operation cancelled by user.',
    214: 'Authentication method not supported. Please try a different authentication type.',
    // ... more mappings
  };
  
  const message = errorMessages[errorCode];
  
  // Don't expose internal error details to users
  if (!message) {
    console.error('Unmapped error code:', errorCode);  // Log for debugging
    return 'An error occurred. Please try again.';    // Generic to user
  }
  
  return message;
}

// โŒ BAD: Exposing internal error details
function badErrorHandling(error) {
  alert(\`Error: \${JSON.stringify(error)}\`);  // Exposes stack traces, internal IDs
}

Comprehensive Error Recovery:

// โœ… GOOD: Multi-layer error handling with recovery
async function handleDataSigningError(error) {
  console.error('Data signing error:', JSON.stringify({
    errorCode: error.error?.longErrorCode,
    statusCode: error.status?.statusCode,
    message: error.error?.errorString
  }, null, 2));
  
  // 1. Attempt SDK state cleanup
  try {
    await DataSigningService.resetState();
  } catch (resetError) {
    console.error('State reset failed:', resetError);
    // Continue - don't block user
  }
  
  // 2. Clear sensitive local state
  DataSigningInputScreen.payload = '';
  DataSigningInputScreen.isLoading = false;
  
  // 3. Show user-friendly error
  const userMessage = DataSigningService.getErrorMessage(
    error.error?.longErrorCode || 0
  );
  DataSigningInputScreen.showError(userMessage);
  
  // 4. Provide retry opportunity
  // Form remains editable for user to retry
}

Production Security Considerations

Audit Logging Requirements:

// โœ… GOOD: Comprehensive audit trail
async function auditDataSigning(request, response) {
  const auditEntry = {
    timestamp: new Date().toISOString(),
    userId: request.userId,
    payloadHash: hashPayload(request.payload),  // Hash, not actual payload
    authLevel: request.authLevel,
    authenticatorType: request.authenticatorType,
    success: response.error.longErrorCode === 0,
    errorCode: response.error.longErrorCode,
    signatureId: response.dataSignatureID,
    sessionId: getCurrentSessionId()
  };
  
  // Send to audit logging service
  await sendToAuditLog(auditEntry);
}

function hashPayload(payload) {
  // Use SHA-256 for audit trail
  // Actual implementation depends on environment
  return crypto.subtle.digest('SHA-256', new TextEncoder().encode(payload))
    .then(hash => Array.from(new Uint8Array(hash))
      .map(b => b.toString(16).padStart(2, '0'))
      .join(''));
}

// โŒ BAD: Logging sensitive data
console.log('Signing payload:', payload);  // Exposes sensitive data in logs

Data Protection Guidelines

Sensitive Data Handling:

// โœ… GOOD: Minimal sensitive data retention
const signingRequest = {
  payloadHash: hashData(payload),    // Store hash instead of original
  authLevel: 4,
  authenticatorType: 1,
  reason: reason,
  timestamp: Date.now()
};

// After signing complete, immediately clear
function clearSensitiveData() {
  signingRequest.payloadHash = null;
  signingRequest.reason = null;
}

// โŒ BAD: Storing original sensitive data indefinitely
const appState = {
  lastPayload: payload,              // Persists in memory
  lastSignature: signature,          // Not encrypted
  userPassword: password             // Never store passwords
};

Data Validation:

// โœ… GOOD: Comprehensive input validation
function validatePayload(payload) {
  // Length check
  if (payload.length > 500) {
    throw new Error('Payload exceeds maximum length');
  }
  
  // Content validation
  if (containsMaliciousContent(payload)) {
    throw new Error('Invalid payload content');
  }
  
  // Checksum validation (if applicable)
  if (!validateChecksum(payload)) {
    throw new Error('Payload integrity check failed');
  }
  
  return true;
}

// โŒ BAD: No validation before processing
function badValidation(payload) {
  // Blindly accepts any input
  return true;
}

Compliance & Regulatory

Industry-Specific Requirements:

Industry

Requirements

Implementation Notes

Financial

SOX, PCI DSS, Basel III

Audit logs, encryption at rest/transit, MFA

Healthcare

HIPAA, HITECH

Patient data protection, access controls, audit trails

Government

FISMA, FedRAMP

High-security authentication (Level 4), certified encryption

Legal

eIDAS, ESIGN Act

Legal-grade signatures, non-repudiation, timestamp authority

Regulatory Compliance Checklist:

Performance & Scalability

Optimization Patterns:

// โœ… GOOD: Debounce user input for character counting
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

const updateCharCount = debounce(() => {
  const count = document.getElementById('payload-count');
  count.textContent = \`\${payload.length}/500\`;
}, 100);

// โœ… GOOD: Cache dropdown options
const DropdownCache = {
  authLevels: null,
  authenticatorTypes: null,
  
  getAuthLevels() {
    if (!this.authLevels) {
      this.authLevels = DropdownDataService.getAuthLevelOptions();
    }
    return this.authLevels;
  }
};

Congratulations! You've successfully implemented secure cryptographic data signing with REL-ID SDK in your Cordova application.

What You've Accomplished

You now have a production-grade data signing system with:

Key Learning Outcomes

  1. Cordova Plugin Integration: How to call native SDK methods via JavaScript bridge with proper callback handling
  2. SPA Architecture: Template-based navigation without page reloads
  3. Event-Driven Integration: DOM events for asynchronous SDK responses
  4. Security Best Practices: Authentication level selection, state cleanup, data protection
  5. Production Patterns: Error handling, audit logging, compliance considerations

Production Deployment Checklist

Before deploying to production, verify:

Security Validation:

Performance & Scalability:

Testing:

Next Steps

Immediate Enhancements:

  1. Batch Signing: Sign multiple documents in sequence
  2. Signature Verification: Verify signed documents
  3. Document Templates: Pre-defined payload templates
  4. Offline Support: Queue signings when offline

Integration Opportunities:

  1. Document Management: Integrate with enterprise document systems
  2. Workflow Automation: Trigger signing via workflow engines
  3. Analytics Dashboard: Track signing metrics and compliance
  4. MDM Integration: Enterprise mobile device management

Advanced Security:

  1. Multi-Signature Workflows: Require multiple signers
  2. Timestamp Authority: Add trusted timestamps to signatures
  3. Certificate Chaining: Link signatures to PKI infrastructure
  4. HSM Integration: Hardware security module backing

Resources

REL-ID Documentation:

Thank you for completing the REL-ID Data Signing Flow codelab! ๐ŸŽ‰

You're now equipped to build secure, production-grade data signing features in your Cordova applications using REL-ID SDK's powerful cryptographic capabilities.