Advanced Features
This guide covers advanced platform features that enable sophisticated widget functionality including OAuth authentication, state sharing between widgets, real-time WebSocket updates, feature flags for conditional rendering, remote module integration, and extension usage. These features allow you to build powerful, interactive widgets that leverage the full capabilities of the platform.
Table of Contents
- Adding OAuth Authentication
- Implementing State Sharing
- Integrating WebSocket Updates
- Adding Feature Flags
- Using Remote Modules
- Working with Extensions
Adding OAuth Authentication
Reference:
The platform provides OAuth authentication through the @invent/proxy-app-components package.
ProxyApp Component
Import:
import { ProxyApp } from "@invent/proxy-app-components";
The ProxyApp component wraps your widget and handles the complete OAuth authentication flow automatically.
Basic Usage
import React from "react";
import { ProxyApp } from "@invent/proxy-app-components";
import type { IWidgetProps } from "../types/widget-props";
const KpiTracker: React.FC<IWidgetProps> = ({
httpClient,
apiUrl,
...otherProps
}) => {
return (
<ProxyApp
httpClient={httpClient}
provider="kpiProvider"
authUrl={apiUrl}
instanceName="kpi-tracker"
>
<KpiTrackerContent {...otherProps} />
</ProxyApp>
);
};
const KpiTrackerContent: React.FC = () => {
// Widget content only renders when authenticated
return (
<div>
<h2>Authenticated KPI Tracker</h2>
{/* Your widget content */}
</div>
);
};
export default KpiTracker;
Advanced Usage with Hooks and Components
For more flexible authentication flows, the package exports additional hooks and components:
Available exports from @invent/proxy-app-components:
import {
ProxyApp, // Main wrapper component
ProxyAppMulti, // Multi-instance authentication
ProxyLogin, // Login UI component
useAuth, // Authentication hook
useAuthContext, // Access auth context
AuthProvider, // Auth context provider
AuthContext, // Auth context
useProviderLogo, // Provider logo hook
} from "@invent/proxy-app-components";
Example with hooks:
import React from "react";
import {
AuthProvider,
useAuth,
ProxyLogin,
} from "@invent/proxy-app-components";
import type { IWidgetProps } from "../types/widget-props";
const KpiTracker: React.FC<IWidgetProps> = ({ httpClient, apiUrl }) => {
return (
<AuthProvider>
<KpiTrackerWithAuth httpClient={httpClient} apiUrl={apiUrl} />
</AuthProvider>
);
};
const KpiTrackerWithAuth: React.FC = ({ httpClient, apiUrl }) => {
const {
isLoggedIn,
setIsLoggedIn,
config,
logout,
availableProviders,
error,
} = useAuth({
httpClient,
provider: "kpiProvider",
authUrl: apiUrl,
instanceName: "kpi-tracker",
});
if (!isLoggedIn) {
return (
<ProxyLogin
httpClient={httpClient}
setIsLoggedIn={setIsLoggedIn}
config={config}
error={error}
/>
);
}
return (
<div>
<h2>Authenticated KPI Tracker</h2>
<button onClick={logout}>Logout</button>
{/* Widget content */}
</div>
);
};
export default KpiTracker;
DAP API - Getting externalConfigurationId
const dapApiUrl = process.env.DAP_API_URL;
const entityType = "account";
const entityIdType = "accountId";
const entityId = searchParams.get("accountId");
const getEntityOrigins = async () => {
const response = await httpClient(`${dapApiUrl}/${entityType}/origins`, {
method: "get",
traceId: "kpi-tracker-origins",
params: {
queryParams: {
[entityIdType]: entityId,
},
},
});
return response;
};
interface Origin {
sourceCode: string;
originalAppId: string;
originalAppTitle: string;
originalAccountId: string;
externalConfigurationId: string; // Pass-through identifier
masterAccountIds: string[];
firmId: string;
}
YARP Proxy API - Making Authenticated Requests
const yarpProxyApiUrl = process.env.YARP_PROXY_API_URL;
const fetchEntityData = async (externalConfigurationId: string) => {
const response = await httpClient(
`${yarpProxyApiUrl}/third_party_endpoint/entities`,
{
method: "get",
traceId: "kpi-tracker-entity",
params: {
queryParams: {
entityId: externalConfigurationId,
},
},
}
);
return response;
};
Complete OAuth Flow
const fetchDataWithAuth = async () => {
try {
// Step 1: Get externalConfigurationId from DAP API
const originsResponse = await getEntityOrigins();
const externalConfigurationId =
originsResponse.data.externalConfigurationId;
// Step 2: Use externalConfigurationId to call third-party API via YARP
const entityData = await fetchEntityData(externalConfigurationId);
console.log("Entity data:", entityData);
return entityData;
} catch (error) {
console.error("Error fetching data:", error);
showNotification("error", "Failed to fetch data", { autoClose: 5000 });
}
};
Implementing State Sharing
Reference: Widget Specifications API
State Sharing Hooks
Three hooks are available for state sharing between widgets:
useShareValue: Set shared value
useSelectSharedValue: Read shared value
useDeleteSharedValue: Delete shared value
Creating a Selector
import { path } from "ramda";
import type { ISharedState } from "@invent/shared-types";
// Selector function
function kpiDataSelector(state: ISharedState) {
return path<IKpiData | undefined>(["kpiData"], state);
}
interface IKpiData {
revenue: number;
users: number;
conversion: number;
timestamp: number;
}
Sharing Data
const ShareDataComponent: React.FC<IWidgetProps> = ({
useShareValue,
httpClient,
apiUrl,
}) => {
const shareValue = useShareValue();
useEffect(() => {
const fetchAndShareData = async () => {
const kpiData = await httpClient(`${apiUrl}/kpi`, {
traceId: "kpi-tracker-share",
});
// Share data with other widgets
shareValue(["kpiData"], kpiData);
};
fetchAndShareData();
}, [httpClient, apiUrl, shareValue]);
return null;
};
Reading Shared Data
const ReadDataComponent: React.FC<IWidgetProps> = ({
useSelectSharedValue,
}) => {
const kpiData = useSelectSharedValue(kpiDataSelector);
return (
<div>
{kpiData ? (
<div>Revenue: ${kpiData.revenue.toLocaleString()}</div>
) : (
<div>No data available</div>
)}
</div>
);
};
Deleting Shared Data
const CleanupComponent: React.FC<IWidgetProps> = ({ useDeleteSharedValue }) => {
const deleteSharedValue = useDeleteSharedValue();
useEffect(() => {
return () => {
// Clean up shared data on unmount
deleteSharedValue(["kpiData"]);
};
}, [deleteSharedValue]);
return null;
};
Complete State Sharing Example
import React, { useEffect } from "react";
import { path } from "ramda";
import type { ISharedState } from "@invent/shared-types";
// Selector
function userProfileSelector(state: ISharedState) {
return path<IUserProfile | undefined>(["userProfile"], state);
}
interface IUserProfile {
id: string;
name: string;
email: string;
}
const UserProfileManager: React.FC<IWidgetProps> = ({
httpClient,
apiUrl,
useShareValue,
useSelectSharedValue,
useDeleteSharedValue,
}) => {
const shareValue = useShareValue();
const userProfile = useSelectSharedValue(userProfileSelector);
const deleteSharedValue = useDeleteSharedValue();
useEffect(() => {
const fetchUserProfile = async () => {
if (apiUrl && !userProfile) {
try {
const profile = await httpClient(`${apiUrl}/user/profile`, {
traceId: "kpi-tracker-profile",
});
shareValue(["userProfile"], profile);
} catch (error) {
console.error("Failed to fetch user profile:", error);
}
}
};
fetchUserProfile();
}, [httpClient, apiUrl, userProfile, shareValue]);
useEffect(() => {
return () => {
// Cleanup on unmount
deleteSharedValue(["userProfile"]);
};
}, [deleteSharedValue]);
return (
<div>
{userProfile ? (
<div>
<p>Name: {userProfile.name}</p>
<p>Email: {userProfile.email}</p>
</div>
) : (
<div>Loading profile...</div>
)}
</div>
);
};
Integrating WebSocket Updates
Reference: BFF-WS-EXT
Extension Configuration (BfF Side)
Note: This is configured on the BfF, not in widgets
import { ExtInitConfig } from "@invent/bff-ws-ext";
export const bffWsExt: ExtInitConfig = {
messages: [],
extHost: `${process.env.MY_HOST}:443`,
qOptions: {
qName: "my-tenant-prefix",
qHost: process.env.RMQ_HOST,
qPort: 5672,
},
};
Getting WebSocket Extension
import { v4 } from "uuid";
const MyWidget: React.FC<IWidgetProps> = ({ getExtension }) => {
const wsExtension = getExtension("@invent/bff-ws-ext");
// ...
};
Subscribing to WebSocket Events
import React, { useEffect } from "react";
import { v4 } from "uuid";
const subscriptionId = v4(); // Create outside component
const WebSocketWidget: React.FC<IWidgetProps> = ({
getExtension,
dashboardWidgetInstanceId,
}) => {
useEffect(() => {
const wss = getExtension("@invent/bff-ws-ext");
if (wss && wss.subscribe) {
const handler = (message: unknown) => {
console.log("WebSocket message received:", message);
// Handle message
};
// Subscribe to entity events
// (entity: string, id: string, callback: (message: unknown) => void) => void
wss.subscribe("KpiUpdate", subscriptionId, handler);
}
return () => {
if (wss && wss.unsubscribe) {
wss.unsubscribe("KpiUpdate", subscriptionId);
}
};
}, [getExtension]);
return <div>Widget with WebSocket</div>;
};
WebSocket Message Structure
interface WebSocketMessage {
message: {
// Message payload
downloadId?: string;
userId?: string;
type?: number;
name?: string;
uri?: string;
status?: number;
expiresAt?: string;
createdAt?: string;
scope?: number;
};
headers: {
"x-datadog-trace-id": string;
"x-datadog-parent-id": string;
"x-datadog-sampling-priority": string;
CausationId: string;
CausedBy: string;
CorrelationId: string;
TenantId: string;
entity: string; // Entity name (e.g., 'Download', 'Notification')
recipientId: string; // User ID
deliveryMethod: string; // 'Direct'
};
}
Complete WebSocket Integration Example
import React, { useEffect, useState } from "react";
import { v4 } from "uuid";
import type { IWidgetProps } from "../types/widget-props";
const subscriptionId = v4();
interface IKpiUpdate {
metric: string;
value: number;
timestamp: number;
}
const KpiTrackerWithWebSocket: React.FC<IWidgetProps> = ({
getExtension,
showNotification,
enableNotifications,
}) => {
const [realtimeData, setRealtimeData] = useState<IKpiUpdate | null>(null);
useEffect(() => {
const wsExt = getExtension("@invent/bff-ws-ext");
if (wsExt && wsExt.subscribe) {
const handleKpiUpdate = (message: any) => {
const update: IKpiUpdate = message.message;
setRealtimeData(update);
if (enableNotifications) {
showNotification(
"success",
`${update.metric} updated to ${update.value}`,
{
position: "bottom-right",
autoClose: 3000,
}
);
}
};
wsExt.subscribe("KpiUpdate", subscriptionId, handleKpiUpdate);
console.log("Subscribed to KPI updates");
}
return () => {
if (wsExt && wsExt.unsubscribe) {
wsExt.unsubscribe("KpiUpdate", subscriptionId);
console.log("Unsubscribed from KPI updates");
}
};
}, [getExtension, enableNotifications, showNotification]);
return (
<div>
<h3>Real-time KPI Updates</h3>
{realtimeData ? (
<div>
<p>
<strong>{realtimeData.metric}</strong>
</p>
<p>Value: {realtimeData.value}</p>
<p>Updated: {new Date(realtimeData.timestamp).toLocaleString()}</p>
</div>
) : (
<p>Waiting for updates...</p>
)}
</div>
);
};
export default KpiTrackerWithWebSocket;
Adding Feature Flags
Reference: Feature Flags
Feature flags enable conditional feature rendering in widgets. Flags are configured in the portal settings and accessed through the platform's feature flag system.
Using Feature Flags with Hooks
import { useFeature, useFeatures } from "flagged";
import { FlagsProvider } from "flagged";
// Single feature
const AdvancedMetrics: React.FC = () => {
const showAdvancedMetrics = useFeature("showAdvancedMetrics");
return (
<div>
{showAdvancedMetrics ? (
<div>Advanced Metrics Dashboard</div>
) : (
<div>Basic Metrics</div>
)}
</div>
);
};
// Multiple features
const FeatureComponent: React.FC = () => {
const features = useFeatures();
return (
<div>
{features.showAdvancedMetrics && <AdvancedSection />}
{features.enableExportFeature && <ExportButton />}
{features.useNewChartLibrary && <NewChart />}
</div>
);
};
Using Feature Flags with Render Props
import { Feature } from "flagged";
const ExportFeature: React.FC = () => {
return (
<Feature name="enableExportFeature">
<ExportButton />
</Feature>
);
};
// With render function
const ConditionalFeature: React.FC = () => {
return (
<Feature name="enableExportFeature">
{(isEnabled) =>
isEnabled ? <ExportButton /> : <p>Export feature coming soon!</p>
}
</Feature>
);
};
Nested Feature Flags
Feature flags support nested objects with path notation:
// Usage
import { useFeature } from "flagged";
const ChartComponent: React.FC = () => {
const showTrends = useFeature("charts.showTrends");
const enableZoom = useFeature("charts.enableZoom");
const pdfExport = useFeature("charts.exportFormats.pdf");
return (
<div>
{showTrends && <TrendLine />}
{enableZoom && <ZoomControls />}
{pdfExport && <PDFExportButton />}
</div>
);
};
Complete Widget with Feature Flags
import React from "react";
import { FlagsProvider, useFeature, Feature } from "flagged";
import type { IWidgetProps } from "../types/widget-props";
const KpiTrackerWithFlags: React.FC<IWidgetProps> = ({ flags, ...props }) => {
// Wrap component with FlagsProvider
return (
<FlagsProvider features={flags || {}}>
<KpiTrackerContent {...props} />
</FlagsProvider>
);
};
const KpiTrackerContent: React.FC<IWidgetProps> = (props) => {
const showAdvancedMetrics = useFeature("showAdvancedMetrics");
const enableExportFeature = useFeature("enableExportFeature");
const betaFeatures = useFeature("betaFeatures");
return (
<div>
<h2>KPI Tracker</h2>
{/* Basic metrics always shown */}
<BasicMetrics />
{/* Advanced metrics with feature flag */}
{showAdvancedMetrics && <AdvancedMetrics />}
{/* Export button with render props */}
<Feature name="enableExportFeature">
{(isEnabled) => isEnabled && <ExportButton />}
</Feature>
{/* Beta features */}
{betaFeatures && (
<div style={{ border: "1px solid orange", padding: "10px" }}>
<p>⚠️ Beta Features</p>
<PredictiveAnalytics />
</div>
)}
</div>
);
};
export default KpiTrackerWithFlags;
Using Remote Modules
Reference: Host application (wlabel) API
Remote modules allow you to render other widgets within your widget.
Generate Widget Data and Render Remote Module
import React, { useMemo, useState } from "react";
import styled from "styled-components";
import type { IWidgetProps } from "../types/widget-props";
const KpiTrackerWithBreadcrumbs: React.FC<IWidgetProps> = ({
remoteModule: RemoteModule,
platformMeta,
apiUrl,
showNotification,
}) => {
const [moduleError, setModuleError] = useState(false);
const breadcrumbsData = useMemo(
() => ({
component: "./Widget",
scope: "acme_breadcrumbs",
host: "https://widgets.example.com/283/remoteEntry.js",
widgetId: "283",
widgetName: "ACME_Breadcrumbs",
}),
[]
);
const handleModuleError = () => {
setModuleError(true);
showNotification("error", "Failed to load breadcrumbs widget", {
autoClose: 5000,
});
};
return (
<WidgetContainer>
{!moduleError ? (
<BreadcrumbsWrapper>
<RemoteModule
module={breadcrumbsData.component}
scope={breadcrumbsData.scope}
url={breadcrumbsData.host}
componentProps={{
apiUrl: apiUrl,
customizationUrl: process.env.CUSTOMIZATION_URL,
}}
onError={handleModuleError}
/>
</BreadcrumbsWrapper>
) : (
<ErrorMessage>Failed to load breadcrumbs component</ErrorMessage>
)}
<MainContent>
{/* KPI Tracker content */}
<h2>KPI Tracker</h2>
</MainContent>
</WidgetContainer>
);
};
const WidgetContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`;
const BreadcrumbsWrapper = styled.div`
margin-bottom: ${getSizeBy(2)};
`;
const MainContent = styled.div`
flex: 1;
`;
const ErrorMessage = styled.div`
padding: ${getSizeBy(1)};
color: ${getColor("danger")};
background-color: ${getColorShade("danger", "lowEmphasis")};
border-radius: ${(props) => props.theme.borderRadius}px;
margin-bottom: ${getSizeBy(2)};
`;
export default KpiTrackerWithBreadcrumbs;
Working with Extensions
Reference: Extensions
Extension Architecture
- Extension: Server part (optional) + Extension Companion (required)
- Extension Companion: A widget without visual part (always a class)
- Extensions: Available to all widgets, always public
Getting Installed Extensions
const MyWidget: React.FC<IWidgetProps> = ({
installedExtensions,
getExtension,
}) => {
// List all installed extensions
const extensionNames = installedExtensions.map((ext) => ext.name);
console.log("Installed extensions:", extensionNames);
return <div>Widget content</div>;
};
Using Extensions
import React, { useEffect } from "react";
import { v4 } from "uuid";
import type { IWidgetProps } from "../types/widget-props";
const WidgetWithExtensions: React.FC<IWidgetProps> = ({
getExtension,
installedExtensions,
}) => {
useEffect(() => {
// Get WebSocket extension
const wsExt = getExtension("@invent/bff-ws-ext");
if (wsExt) {
const subscriptionId = v4();
wsExt.subscribe("Notification", subscriptionId, (message: unknown) => {
console.log("Extension message:", message);
});
return () => {
wsExt.unsubscribe("Notification", subscriptionId);
};
}
}, [getExtension]);
return (
<div>
<h3>Available Extensions</h3>
<ul>
{installedExtensions.map((ext, index) => (
<li key={index}>
{ext.name} - v{ext.version}
</li>
))}
</ul>
</div>
);
};
Preset Extension
Reference: Preset Extension
The Preset Extension provides access to preconfigured widget settings (also known as Derived Micro Apps).
interface IPresetExtension {
presetsByWidgetName: (widgetTypeName: string) => unknown[];
knownPresets: () => string[];
}
const WidgetWithPresets: React.FC<IWidgetProps> = ({ getExtension }) => {
useEffect(() => {
const presetExt = getExtension("@invent/preset-ext");
if (presetExt) {
// Get available presets for this widget
const presets = presetExt.presetsByWidgetName("ACME_KpiTracker");
// Get all known preset names
const knownPresets = presetExt.knownPresets();
console.log("Available presets:", presets);
console.log("Known presets:", knownPresets);
}
}, [getExtension]);
return <div>Widget with presets</div>;
};
Congratulations!
You've completed the widget development tutorial! You now have the knowledge to build sophisticated widgets with all platform features.
Additional Resources
- Platform Overview
- Experience Builder Architecture
- Widget Guidelines
- UI-Kit Documentation
- Extensions Documentation
Happy Widget Development!