Integration Guide

There are two possible integration approaches:

Option 1: Embedded Wallet App

In this approach, the Wallet App is embedded directly inside the Beneficiary App (as a WebView or native module).

Workflow

  1. Beneficiary App launches the embedded Wallet UI.

  2. User onboards into the Wallet (via Dhiway).

  3. User adds/imports VCs directly through the Wallet interface.

  4. Beneficiary App fetches VC references from the embedded Wallet context.

Advantages

  • ✅ Faster integration (minimal code changes in Beneficiary App).

  • ✅ Secure by design (Wallet handles onboarding, VC storage, compliance).

  • ✅ Standard UI/UX for Wallet functions.

Considerations

  • ❌ Limited customization (UI/UX is dictated by Wallet app).

  • ❌ Tight coupling — updating Wallet UI may affect Beneficiary app.

Complete Integration Flow

1. Initial Setup & Environment Configuration

Beneficiary App (Parent) Configuration

// Environment variables in parent app (To communicate with wallet app)
// Wallet service and CORS settings
VITE_EWALLET_ORIGIN=https://wallet.yourdomain.com
VITE_EWALLET_IFRAME_SRC=https://wallet.yourdomain.com

Wallet App (Child) Configurationjavascript

// Environment variables in wallet UI (To communicate with parent app)
REACT_APP_PARENT_APP_ALLOWED_ORIGIN=https://beneficiary.yourdomain.com

2. Authentication Token Management

Step 1: Wallet Token Storage in Beneficiary App (Parent App)

The beneficiary/parent app can store the user's wallet service token, and when the user wants to use the wallet app the beneficiary app can fetch the user's wallet token and store the wallet authentication token in localStorage. Now it can be used while opening the wallet app in an iframe in embedded mode:

const openWalletUI = () => {
    localStorage.setItem('embeddedMode', 'true');
    
    const walletToken = localStorage.getItem('walletToken');
    const user = localStorage.getItem('user');
    
    if (!walletToken || !user) {
        setError('Unable to connect to wallet service. Please try logging in again.');
        return;
    }
    
    // Open iframe with wallet UI in the parent app
};

Step 2: Iframe Creation and Authentication Passingtypescript

// Beneficiary app creates iframe and sends auth data
const sendAuthToIframe = () => {
    if (!iframeRef.current) return;
    
    const walletToken = localStorage.getItem('walletToken');
    const userStr = localStorage.getItem('user');
    
    // Parse user data
    let user;
    try {
        user = JSON.parse(userStr);
    } catch {
        setError('Invalid user data found.');
        return;
    }

    // Create authentication message
    const messageData = {
        type: 'WALLET_AUTH',
        data: {
            walletToken: walletToken,
            user: user,
            embeddedMode: true,
        },
    };

    // Send to wallet iframe with origin validation
    const targetOrigin = new URL(VITE_EWALLET_IFRAME_SRC).origin;
    iframeRef.current.contentWindow?.postMessage(
        messageData,
        targetOrigin
    );
};

Step 3: Wallet App Receives Authentication

useEffect(() => {
    const handleMessage = (event) => {
        // Security: Validate origin
        const allowedOrigin = process.env.REACT_APP_PARENT_APP_ALLOWED_ORIGIN;
        if (event.origin !== allowedOrigin) {
            console.warn('Rejected message from untrusted origin:', event.origin);
            return;
        }

        if (event.data?.type === 'WALLET_AUTH' && event.data?.data) {
            const { walletToken, user: userData, embeddedMode } = event.data.data;
            
            if (walletToken && userData) {
                // Store authentication data
                localStorage.setItem('walletToken', walletToken);
                localStorage.setItem('user', JSON.stringify(userData));
                if (embeddedMode) {
                    localStorage.setItem('embeddedMode', 'true');
                }

                // Update state and API headers
                setToken(walletToken);
                setUser(userData);
                setWaitingForParentAuth(false);
                setLoading(false);
                
                // Set token in API headers
                api.defaults.headers.common['Authorization'] = `Bearer ${walletToken}`;
            }
        }
    };

    // Listen for messages from parent
    if (isEmbedded) {
        window.addEventListener('message', handleMessage);
    }
}, [isEmbedded]);

Once this is done, you will be able to log in already logged-in user and access their wallet app within the beneficiary / parent app itself

Step 4: Wallet App Fetches VCs

Step 5: User Selects VCs to Share

Once the VCs are shared by the wallet app using the postMessage, now the parent app needs to listen to the data shared from the wallet app

Step 6: Message Listener in Beneficiary App

useEffect(() => {
    const handleMessage = async (event: MessageEvent) => {
        // Security: Validate origin
        if (event.origin !== VITE_EWALLET_ORIGIN) return;

        const { type, data } = event.data;
        console.log('Received message from wallet:', type, data);

        if (type === 'VC_SHARED') {
            try {
                await handleVCShared(data, processingToastIdRef);
            } catch (error) {
                console.error('Error handling VC_SHARED:', error);
                // Show error toast
                toast({
                    title: 'Error',
                    description: error.message ?? 'Failed to process documents',
                    status: 'error',
                    duration: 5000,
                });
            }
        }
    };

    window.addEventListener('message', handleMessage);
    return () => window.removeEventListener('message', handleMessage);
}, []);

Step 7: VC Data Processing

Now the beneficiary app can process the revived VC data to store it or use it as needed

Option 2: Direct Wallet API Integration

In this approach, the Beneficiary App communicates with the Wallet Service APIs directly.

a Postman collection is available here:

👉 UBI Wallet Middleware Postman Collection

1. User Onboarding

POST /api/wallet/onboard
const onboardUser = async (userData) => {
  try {
    const response = await fetch('http://localhost:3018/api/wallet/onboard', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData)
    });

    const result = await response.json();
    
    if (result.statusCode === 200) {
      console.log('User onboarded successfully:', result.data);
      return result.data;
    } else {
      throw new Error(result.message);
    }
  } catch (error) {
    console.error('Onboarding failed:', error);
    throw error;
  }
};

// Usage
const userData = {
  firstName: "John",
  lastName: "Doe",
  username: "johndoe",
  password: "SecurePass123!",
  email: "john@example.com",
  phone: "+1234567890"
};

onboardUser(userData);

2. User Login

POST /api/wallet/login
const loginUser = async (credentials) => {
  try {
    const response = await fetch('http://localhost:3018/api/wallet/login', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(credentials)
    });

    const result = await response.json();
    
    if (result.statusCode === 200) {
      // Store token for future API calls
      localStorage.setItem('walletToken', result.data.token);
      localStorage.setItem('user', JSON.stringify(result.data.user));
      console.log('Login successful:', result.data);
      return result.data;
    } else {
      throw new Error(result.message);
    }
  } catch (error) {
    console.error('Login failed:', error);
    throw error;
  }
};

// Usage
const credentials = {
  username: "johndoe",
  password: "SecurePass123!"
};

loginUser(credentials);

3. Fetch VC List

GET /api/wallet/{accountId}/vcs
const fetchVCs = async (accountId, token) => {
  try {
    const response = await fetch(`http://localhost:3018/api/wallet/${accountId}/vcs`, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      }
    });

    const result = await response.json();
    
    if (result.statusCode === 200) {
      console.log('VCs retrieved successfully:', result.data);
      return result.data;
    } else {
      throw new Error(result.message);
    }
  } catch (error) {
    console.error('Failed to fetch VCs:', error);
    throw error;
  }
};

// Usage
const accountId = "ext_user_123";
const token = localStorage.getItem('walletToken');
fetchVCs(accountId, token);

4. Get VC Details

GET /api/wallet/{accountId}/vcs/{vcId}
const getVCDetails = async (accountId, vcId, token) => {
  try {
    const response = await fetch(`http://localhost:3018/api/wallet/${accountId}/vcs/${vcId}`, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      }
    });

    const result = await response.json();
    
    if (result.statusCode === 200) {
      console.log('VC details retrieved successfully:', result.data);
      return result.data;
    } else {
      throw new Error(result.message);
    }
  } catch (error) {
    console.error('Failed to fetch VC details:', error);
    throw error;
  }
};

// Usage
const accountId = "ext_user_123";
const vcId = "vc_123";
const token = localStorage.getItem('walletToken');
getVCDetails(accountId, vcId, token);

Security Best Practices

  1. Token Storage: Store authentication tokens securely (consider using httpOnly cookies in production)

  2. HTTPS: Always use HTTPS in production environments

  3. Input Validation: Validate all user inputs before sending to API

  4. Error Handling: Implement proper error handling for all API calls

  5. Rate Limiting: Implement client-side rate limiting to prevent abuse

Last updated