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
Beneficiary App launches the embedded Wallet UI.
User onboards into the Wallet (via Dhiway).
User adds/imports VCs directly through the Wallet interface.
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
Token Storage: Store authentication tokens securely (consider using httpOnly cookies in production)
HTTPS: Always use HTTPS in production environments
Input Validation: Validate all user inputs before sending to API
Error Handling: Implement proper error handling for all API calls
Rate Limiting: Implement client-side rate limiting to prevent abuse
Last updated