This codelab demonstrates how to implement the REL-ID Initialization flow using the cordova-plugin-rdna Cordova plugin. The REL-ID SDK provides secure identity verification and session management for mobile applications.

What You'll Learn

What You'll Need

Get the Code from GitHub

The code to get started is stored 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-initialize folder in the repository you cloned earlier

This codelab uses Single Page Application (SPA) architecture for optimal performance and user experience.

Why SPA?

SPA Benefits:

SPA Key Components

Component

Purpose

Location

index.html

Single HTML with templates

www/index.html

NavigationService

Template swapping

www/src/tutorial/navigation/NavigationService.js

AppInitializer

One-time SDK init

www/src/uniken/AppInitializer.js

Screen modules

onContentLoaded() lifecycle

www/src/tutorial/screens/*.js

SPA Flow

deviceready → AppInitializer.initialize() → NavigationService.navigate('TutorialHome')
  ↓
Template swapped → TutorialHomeScreen.onContentLoaded() → User clicks Initialize
  ↓
SDK initialized → onInitialized event → Navigate to TutorialSuccess
  ↓
Template swapped → TutorialSuccessScreen.onContentLoaded(sessionData)

Before implementing your own REL-ID initialization, let's examine the sample app structure to understand the recommended SPA architecture:

Component

Purpose

Sample App Reference

Connection Profile

Configuration data

www/src/uniken/cp/agent_info.json

Profile Parser

Utility to parse connection data

www/src/uniken/utils/connectionProfileParser.js

Event Manager

Handles SDK callbacks

www/src/uniken/services/rdnaEventManager.js

REL-ID Service

Main SDK interface

www/src/uniken/services/rdnaService.js

Event Provider

Global event handling

www/src/uniken/providers/SDKEventProvider.js

App Initializer

One-time SDK handler registration

www/src/uniken/AppInitializer.js

Navigation Service

SPA template swapping

www/src/tutorial/navigation/NavigationService.js

UI Screens

User interface screens

www/src/tutorial/screens/TutorialHomeScreen.js

Recommended Directory Structure

Create the following directory structure in your Cordova project:

www/
├── index.html                          # Single HTML with templates
├── css/
│   └── index.css                       # All styles
├── js/
│   └── app.js                          # deviceready bootstrap
└── src/
    ├── uniken/
    │   ├── cp/
    │   │   └── agent_info.json
    │   ├── services/
    │   │   ├── rdnaEventManager.js
    │   │   └── rdnaService.js
    │   ├── utils/
    │   │   ├── connectionProfileParser.js
    │   │   └── progressHelper.js
    │   ├── providers/
    │   │   └── SDKEventProvider.js
    │   └── AppInitializer.js           # SPA initializer
    └── tutorial/
        ├── navigation/
        │   └── NavigationService.js    # SPA navigation
        └── screens/
            ├── TutorialHomeScreen.js
            ├── TutorialSuccessScreen.js
            └── TutorialErrorScreen.js

Project Structure

This project includes a local Cordova plugin. Ensure the plugin directory exists in your project root:

# Verify plugin directory exists
ls -la ./RdnaClient

Add the REL-ID Plugin (Local)

The SDK plugin is included as a local plugin in the project. Install it from the local directory:

cordova plugin add ./RdnaClient

Add File Plugin

For loading local JSON files, install cordova-plugin-file:

cordova plugin add cordova-plugin-file

Platform Setup

# Add platforms
cordova platform add ios
cordova platform add android

# Prepare platforms
cordova prepare

Follow the Cordova platform setup guide for platform-specific configuration.

agent_info.json

Create your connection profile JSON file in www/src/uniken/cp/agent_info.json:

{
  "RelIds": [
    {
      "Name": "YourRELIDAgentName",
      "RelId": "your-rel-id-string-here"
    }
  ],
  "Profiles": [
    {
      "Name": "YourRELIDAgentName",
      "Host": "your-gateway-host.com",
      "Port": "443"
    }
  ]
}

Security Considerations

The connection profile parser loads and validates the agent_info.json file using cordova-plugin-file.

Create www/src/uniken/utils/connectionProfileParser.js:

/**
 * Parses agent info data and extracts connection profile
 * @param {Object} profileData - Raw agent info data
 * @returns {Object} Parsed connection profile
 */
function parseAgentInfo(profileData) {
  if (!profileData.RelIds || profileData.RelIds.length === 0) {
    throw new Error('No RelIds found in agent info');
  }

  if (!profileData.Profiles || profileData.Profiles.length === 0) {
    throw new Error('No Profiles found in agent info');
  }

  // Always pick the first array objects
  const firstRelId = profileData.RelIds[0];

  if (!firstRelId.Name || !firstRelId.RelId) {
    throw new Error('Invalid RelId object - missing Name or RelId');
  }

  // Find matching profile by Name (1-1 mapping)
  const matchingProfile = profileData.Profiles.find(
    profile => profile.Name === firstRelId.Name
  );

  if (!matchingProfile) {
    throw new Error(`No matching profile found for RelId name: ${firstRelId.Name}`);
  }

  if (!matchingProfile.Host || !matchingProfile.Port) {
    throw new Error('Invalid Profile object - missing Host or Port');
  }

  // Convert port to number if it's a string
  const port = typeof matchingProfile.Port === 'string'
    ? parseInt(matchingProfile.Port, 10)
    : matchingProfile.Port;

  if (isNaN(port)) {
    throw new Error(`Invalid port value: ${matchingProfile.Port}`);
  }

  return {
    relId: firstRelId.RelId,
    host: matchingProfile.Host,
    port: port
  };
}

/**
 * Loads agent info from file using cordova-plugin-file
 */
async function loadAgentInfo() {
  return new Promise((resolve, reject) => {
    try {
      const basePath = cordova.file.applicationDirectory + 'www/';
      const filePath = basePath + 'src/uniken/cp/agent_info.json';

      console.log('ConnectionProfileParser - Loading file from:', filePath);

      // Resolve file path and read with FileReader
      window.resolveLocalFileSystemURL(
        filePath,
        (fileEntry) => {
          fileEntry.file(
            (file) => {
              const reader = new FileReader();

              reader.onloadend = function() {
                try {
                  const profileData = JSON.parse(this.result);
                  const parsed = parseAgentInfo(profileData);
                  resolve(parsed);
                } catch (error) {
                  reject(new Error(`Failed to parse: ${error.message}`));
                }
              };

              reader.onerror = (error) => reject(error);
              reader.readAsText(file);
            },
            (error) => reject(error)
          );
        },
        (error) => reject(error)
      );
    } catch (error) {
      reject(new Error(`Failed to load agent info: ${error.message}`));
    }
  });
}

The parser performs several critical functions:

The event manager handles all REL-ID SDK callbacks using DOM events and a singleton pattern.

Create www/src/uniken/services/rdnaEventManager.js:

/**
 * REL-ID SDK Event Manager
 * Manages all SDK events using document.addEventListener()
 */
class RdnaEventManager {
  constructor() {
    if (RdnaEventManager.instance) {
      return RdnaEventManager.instance;
    }

    this._initialized = false;
    this.listeners = [];
    this.initializeProgressHandler = null;
    this.initializeErrorHandler = null;
    this.initializedHandler = null;

    RdnaEventManager.instance = this;
  }

  static getInstance() {
    if (!RdnaEventManager.instance) {
      RdnaEventManager.instance = new RdnaEventManager();
    }
    return RdnaEventManager.instance;
  }

  /**
   * Initialize event listeners (idempotent - safe to call multiple times)
   * In SPA architecture, this is called ONCE in AppInitializer
   */
  initialize() {
    if (this._initialized) {
      console.log('RdnaEventManager - Already initialized, skipping');
      return;
    }

    console.log('RdnaEventManager - Initializing event listeners');
    this.registerEventListeners();
    this._initialized = true;
  }

  registerEventListeners() {
    console.log('RdnaEventManager - Registering native event listeners');

    const progressListener = this.onInitializeProgress.bind(this);
    const errorListener = this.onInitializeError.bind(this);
    const initializedListener = this.onInitialized.bind(this);

    document.addEventListener('onInitializeProgress', progressListener, false);
    document.addEventListener('onInitializeError', errorListener, false);
    document.addEventListener('onInitialized', initializedListener, false);

    this.listeners.push(
      { name: 'onInitializeProgress', handler: progressListener },
      { name: 'onInitializeError', handler: errorListener },
      { name: 'onInitialized', handler: initializedListener }
    );

    console.log('RdnaEventManager - Native event listeners registered');
  }

  onInitializeProgress(event) {
    console.log("RdnaEventManager - Initialize progress event received");

    try {
      const progressData = JSON.parse(event.response);
      console.log("RdnaEventManager - Progress:", progressData.initializeStatus);

      if (this.initializeProgressHandler) {
        this.initializeProgressHandler(progressData);
      }
    } catch (error) {
      console.error("RdnaEventManager - Failed to parse progress:", error);
    }
  }

  onInitializeError(event) {
    console.log("RdnaEventManager - Initialize error event received");

    try {
      const errorData = JSON.parse(event.response);
      console.error("RdnaEventManager - Initialize error:", errorData.errorString);

      if (this.initializeErrorHandler) {
        this.initializeErrorHandler(errorData);
      }
    } catch (error) {
      console.error("RdnaEventManager - Failed to parse error:", error);
    }
  }

  onInitialized(event) {
    console.log("RdnaEventManager - Initialize success event received");

    try {
      const initializedData = JSON.parse(event.response);
      console.log("RdnaEventManager - Successfully initialized, Session ID:", initializedData.session.sessionID);

      if (this.initializedHandler) {
        this.initializedHandler(initializedData);
      }
    } catch (error) {
      console.error("RdnaEventManager - Failed to parse success:", error);
    }
  }

  setInitializeProgressHandler(callback) {
    this.initializeProgressHandler = callback;
  }

  setInitializeErrorHandler(callback) {
    this.initializeErrorHandler = callback;
  }

  setInitializedHandler(callback) {
    this.initializedHandler = callback;
  }

  cleanup() {
    console.log('RdnaEventManager - Cleaning up event listeners');

    this.listeners.forEach(listener => {
      document.removeEventListener(listener.name, listener.handler, false);
    });
    this.listeners = [];

    this.initializeProgressHandler = null;
    this.initializeErrorHandler = null;
    this.initializedHandler = null;

    console.log('RdnaEventManager - Cleanup completed');
  }
}

Key features of the event manager:

The REL-ID service provides the main interface for SDK operations using the Cordova plugin API.

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

class RdnaService {
  constructor() {
    if (RdnaService.instance) {
      return RdnaService.instance;
    }

    this.eventManager = null;
    RdnaService.instance = this;
  }

  static getInstance() {
    if (!RdnaService.instance) {
      RdnaService.instance = new RdnaService();
    }
    return RdnaService.instance;
  }

  getEventManager() {
    if (!this.eventManager) {
      this.eventManager = RdnaEventManager.getInstance();
    }
    return this.eventManager;
  }

  async getSDKVersion() {
    return new Promise((resolve, reject) => {
      com.uniken.rdnaplugin.RdnaClient.getSDKVersion(
        (response) => {
          try {
            const parsed = JSON.parse(response);
            const version = parsed?.response || 'Unknown';
            console.log('RdnaService - SDK Version:', version);
            resolve(version);
          } catch (error) {
            reject(new Error('Failed to parse SDK version response'));
          }
        },
        (error) => reject(error),
        []
      );
    });
  }

  async initialize() {
    const profile = await loadAgentInfo();
    console.log('RdnaService - Loaded connection profile:', JSON.stringify({
      host: profile.host,
      port: profile.port,
      relId: profile.relId.substring(0, 10) + '...',
    }, null, 2));

    return new Promise((resolve, reject) => {
      com.uniken.rdnaplugin.RdnaClient.initialize(
        (response) => {
          try {
            const result = JSON.parse(response);

            if (result.error && result.error.longErrorCode === 0) {
              console.log('RdnaService - Sync response success');
              resolve(result);
            } else {
              reject(result);
            }
          } catch (error) {
            reject(new Error('Failed to parse initialize response'));
          }
        },
        (error) => reject(error),
        [
          profile.relId,    // 0: agentInfo
          profile.host,     // 1: gatewayHost
          profile.port,     // 2: gatewayPort
          '',               // 3: cipherSpecs
          '',               // 4: cipherSalt
          '',               // 5: proxySettings
          '',               // 6: sslCertificate
          com.uniken.rdnaplugin.RdnaClient.RDNALoggingLevel.RDNA_NO_LOGS  // 7: logLevel
        ]
      );
    });
  }
}

const rdnaService = RdnaService.getInstance();

Important API Parameters

The com.uniken.rdnaplugin.RdnaClient.initialize() call requires specific parameter ordering:

Parameter

Purpose

Example

agentInfo

RelId

From connection profile

gatewayHost

Server hostname

From connection profile

gatewayPort

Server port

From connection profile

logLevel

Logging setting

RDNA_NO_LOGS for production

How Cordova Plugins Work

Cordova plugins are loaded automatically - no imports needed.

Loading Flow (Local Plugin):

  1. Install: cordova plugin add ./RdnaClient
  2. Build: Plugin registers JavaScript interface
  3. Access: Use global namespace com.uniken.rdnaplugin.RdnaClient
// ❌ NOT NEEDED (no imports for local plugins)
import RdnaClient from './RdnaClient';

// ✅ CORRECT - Direct global access (same for local and remote plugins)
com.uniken.rdnaplugin.RdnaClient.getSDKVersion(successCallback, errorCallback, []);

Script Loading Order

In your HTML files, maintain this order:

<!-- 1. Cordova core -->
<script src="cordova.js"></script>

<!-- 2. Utilities -->
<script src="src/uniken/utils/connectionProfileParser.js"></script>
<script src="src/uniken/utils/progressHelper.js"></script>

<!-- 3. Services -->
<script src="src/uniken/services/rdnaEventManager.js"></script>
<script src="src/uniken/services/rdnaService.js"></script>

<!-- 4. Providers -->
<script src="src/uniken/providers/SDKEventProvider.js"></script>

<!-- 5. App Initializer -->
<script src="src/uniken/AppInitializer.js"></script>

<!-- 6. Navigation -->
<script src="src/tutorial/navigation/NavigationService.js"></script>

<!-- 7. Screens -->
<script src="src/tutorial/screens/TutorialHomeScreen.js"></script>

<!-- 8. Bootstrap -->
<script src="js/app.js"></script>

The App Initializer is the cornerstone of SPA architecture - it registers all SDK handlers ONCE.

Create www/src/uniken/AppInitializer.js:

const AppInitializer = {
  _initialized: false,

  /**
   * Initialize all SDK event handlers (call ONCE in app.js)
   * Safe to call multiple times - will skip if already initialized
   */
  initialize() {
    if (this._initialized) {
      console.log('AppInitializer - Already initialized, skipping');
      return;
    }

    console.log('AppInitializer - Initializing SDK handlers');

    try {
      // Step 1: Initialize event manager
      const eventManager = rdnaService.getEventManager();
      eventManager.initialize();

      // Step 2: Initialize SDK event provider
      SDKEventProvider.initialize();

      this._initialized = true;
      console.log('AppInitializer - SDK handlers successfully initialized');
      console.log('AppInitializer - Handlers will persist for entire app lifecycle');
    } catch (error) {
      console.error('AppInitializer - Failed to initialize:', error);
      throw error;
    }
  }
};

SPA Architecture Benefits:

The Navigation Service handles screen transitions using template swapping instead of page reloads.

Create www/src/tutorial/navigation/NavigationService.js:

const NavigationService = {
  currentRoute: null,

  /**
   * Navigate to a screen with optional parameters
   */
  navigate(routeName, params) {
    console.log('NavigationService - Navigating to:', routeName);
    this.currentRoute = routeName;
    this.loadScreenContent(routeName, params || {});
  },

  /**
   * Load screen content from template (SPA pattern)
   */
  loadScreenContent(routeName, params) {
    // Get template element (ID format: "TutorialHome-template")
    const templateId = `${routeName}-template`;
    const template = document.getElementById(templateId);

    if (!template) {
      console.error('NavigationService - Template not found:', templateId);
      return;
    }

    // Clone template content
    const content = template.content.cloneNode(true);

    // Get app content container
    const container = document.getElementById('app-content');
    if (!container) {
      console.error('NavigationService - App content container not found');
      return;
    }

    // Replace container content (SPA magic - no page reload!)
    container.innerHTML = '';
    container.appendChild(content);

    // Initialize screen with params
    const screenObj = window[`${routeName}Screen`];
    if (screenObj && typeof screenObj.onContentLoaded === 'function') {
      screenObj.onContentLoaded(params);
    }
  }
};

SPA Pattern Explanation:

In SPA architecture, each screen consists of:

  1. HTML template in index.html
  2. JavaScript module with onContentLoaded() lifecycle

index.html Structure

Create www/index.html with SPA structure:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>REL-ID Integration Tutorial</title>
    <link rel="stylesheet" href="css/index.css">
</head>
<body>
    <!-- SPA Content Container -->
    <div id="app-content"></div>

    <!-- Template: Tutorial Home Screen -->
    <template id="TutorialHome-template">
        <div class="container">
            <div class="header">
                <h1>REL-ID Integration Tutorial</h1>
                <p>Learn cordova-plugin-rdna Integration</p>
            </div>

            <div class="card">
                <h2>SDK Information</h2>
                <span>SDK Version: </span>
                <span id="sdk-version">Loading...</span>
            </div>

            <button id="initialize-btn">Initialize</button>

            <div id="progress-container" style="display: none;">
                <p id="progress-text">Initializing...</p>
            </div>
        </div>
    </template>

    <!-- All Scripts (loaded once) -->
    <script src="cordova.js"></script>
    <script src="src/uniken/utils/connectionProfileParser.js"></script>
    <script src="src/uniken/utils/progressHelper.js"></script>
    <script src="src/uniken/services/rdnaEventManager.js"></script>
    <script src="src/uniken/services/rdnaService.js"></script>
    <script src="src/uniken/providers/SDKEventProvider.js"></script>
    <script src="src/uniken/AppInitializer.js"></script>
    <script src="src/tutorial/navigation/NavigationService.js"></script>
    <script src="src/tutorial/screens/TutorialHomeScreen.js"></script>
    <script src="src/tutorial/screens/TutorialSuccessScreen.js"></script>
    <script src="src/tutorial/screens/TutorialErrorScreen.js"></script>
    <script src="js/app.js"></script>
</body>
</html>

Screen JavaScript Module

Create www/src/tutorial/screens/TutorialHomeScreen.js:

const TutorialHomeScreen = {
  isInitializing: false,

  /**
   * Called when screen content is loaded into DOM (SPA lifecycle)
   */
  onContentLoaded(params) {
    console.log('TutorialHomeScreen - Content loaded');

    this.isInitializing = false;
    this.loadSDKVersion();
    this.setupEventListeners();
    this.registerSDKEventHandlers();
  },

  setupEventListeners() {
    const initButton = document.getElementById('initialize-btn');
    if (initButton) {
      initButton.onclick = this.handleInitializePress.bind(this);
    }
  },

  registerSDKEventHandlers() {
    const eventManager = rdnaService.getEventManager();

    eventManager.setInitializeProgressHandler((data) => {
      const message = getProgressMessage(data);
      this.updateProgress(message);
    });

    eventManager.setInitializeErrorHandler((errorData) => {
      this.isInitializing = false;
      this.updateButtonState(false);
      this.hideProgress();

      NavigationService.navigate('TutorialError', {
        shortErrorCode: errorData.shortErrorCode,
        longErrorCode: errorData.longErrorCode,
        errorString: errorData.errorString
      });
    });
  },

  async loadSDKVersion() {
    const versionElement = document.getElementById('sdk-version');
    if (!versionElement) return;

    try {
      versionElement.textContent = 'Loading...';
      const version = await rdnaService.getSDKVersion();
      versionElement.textContent = version;
    } catch (error) {
      versionElement.textContent = 'Unknown';
    }
  },

  handleInitializePress() {
    if (this.isInitializing) return;

    this.isInitializing = true;
    this.updateButtonState(true);
    this.showProgress('Starting REL-ID initialization...');

    rdnaService.initialize()
      .then((syncResponse) => {
        console.log('Sync response success - waiting for async events');
      })
      .catch((error) => {
        this.isInitializing = false;
        this.updateButtonState(false);
        this.hideProgress();
        alert(`Initialization Failed\n\n${error.error?.errorString}`);
      });
  },

  updateButtonState(isLoading) {
    const button = document.getElementById('initialize-btn');
    if (button) {
      button.disabled = isLoading;
      button.textContent = isLoading ? 'Initializing...' : 'Initialize';
    }
  },

  showProgress(message) {
    const container = document.getElementById('progress-container');
    const textElement = document.getElementById('progress-text');
    if (container && textElement) {
      textElement.textContent = message;
      container.style.display = 'block';
    }
  },

  hideProgress() {
    const container = document.getElementById('progress-container');
    if (container) {
      container.style.display = 'none';
    }
  }
};

// Expose to global scope for NavigationService
window.TutorialHomeScreen = TutorialHomeScreen;

App Bootstrap

Create www/js/app.js:

const App = {
  initialize() {
    document.addEventListener('deviceready', this.onDeviceReady.bind(this), false);
  },

  onDeviceReady() {
    console.log('App - Device ready');

    // Initialize SDK handlers ONCE (SPA magic!)
    AppInitializer.initialize();

    // Navigate to home screen
    NavigationService.navigate('TutorialHome');

    console.log('App - SDK handlers will persist for entire app lifecycle');
  }
};

App.initialize();

Key Implementation Features

SPA Lifecycle:

State Management:

Event-Driven Architecture:

The following images showcase screens from the sample application:

Initialize Progress Screen

Initialize Error Screen

Initialize Screen

Build and Run

# Prepare platforms
cordova prepare

# Run on iOS
cordova run ios

# Run on Android
cordova run android

Debugging

iOS: Safari → Develop → Simulator → [Your App] Android: Chrome → chrome://inspect → Inspect

Key Test Scenarios

  1. Successful Initialization: Verify the complete flow works end-to-end
  2. Network Errors: Test with invalid host/port configurations
  3. Invalid Credentials: Test with incorrect RelId values
  4. Progress Tracking: Verify progress events are properly displayed

Verification Steps

Test your implementation with console logging:

// Test SDK version retrieval
rdnaService.getSDKVersion()
  .then((version) => console.log('SDK Version:', version))
  .catch((error) => console.error('Version failed:', error));

// Test initialization
rdnaService.initialize()
  .then((syncResponse) => console.log('Initialization successful:', JSON.stringify(syncResponse, null, 2)))
  .catch((error) => console.error('Initialization failed:', error));

Connection Profile Issues

Error: "No RelIds found in agent info" Solution: Verify your JSON structure matches the sample in www/src/uniken/cp/agent_info.json

Error: "No matching profile found for RelId name" Solution: Ensure the Name field in RelIds matches the Name field in Profiles

Cordova-Specific Issues

"Can't find variable: com"

"Plugin not found"

Local plugin installation fails

File loading fails on iOS

Events not firing

Changes not reflecting

Screen not loading (SPA)

Network Connectivity

Error: Network connection failures Solution: Check host/port values and network accessibility

Error: REL-ID connection issues Solution: Verify the REL-ID server is set up and running

SDK Initialization

Error Code 88: SDK already initialized Solution: Terminate the SDK

Error Code 288: SDK detected dynamic attack performed on the app Solution: Terminate the app

Error Code 179: Initialization in progress Solution: Wait for current initialization to complete before retrying

Security Considerations

Memory Management

Performance Optimization

User Experience

Congratulations! You've successfully learned how to implement REL-ID SDK initialization in Cordova with:

Key Cordova Patterns Learned

Next Steps