Learn how to use delegates to receive events and customize behavior from the Agentforce SDK in your React Native application.
Overview
Delegates allow your JavaScript code to receive events and override behavior from the native Agentforce SDK. The bridge supports three delegate types:
| Delegate | Purpose | Direction |
|---|---|---|
| Logger | Receive SDK log messages | Native → JS |
| Navigation | Handle navigation requests from the agent | Native → JS |
| View Provider | Override native SDK views with React Native components | JS → Native (registration) + Native → JS (rendering) |
All delegates use a NativeEventEmitter on the JS side to receive events from the native layer.
Why Register Before configure()?
The recommended order is to register delegates before calling configure(). Here's why:
- Logger: The SDK may emit log messages during initialization. If registered after
configure(), those early messages are lost. - Navigation: The navigation bridge is passed to
AgentforceConfigurationduringconfigure(). Registering afterward still works because the bridge checks dynamically, but registering before is cleaner. - View Provider: Can be registered before or after
configure()without issue. The native view provider checks the component map dynamically.
Logger Delegate
The Logger delegate forwards log messages from the native Agentforce SDK to your JavaScript code. This is useful for debugging, analytics, and monitoring.
Setup
import { AgentforceService } from "@salesforce/react-native-agentforce";
AgentforceService.setLoggerDelegate({
onLog(level, message, error) {
const prefix = `[Agentforce ${level.toUpperCase()}]`;
if (error) {
console.log(`${prefix} ${message} | Error: ${error}`);
} else {
console.log(`${prefix} ${message}`);
}
},
});
Parameters
| Parameter | Type | Description |
|---|---|---|
level |
'error' | 'warn' | 'info' | 'debug' |
The severity level of the log message. |
message |
string |
The log message from the SDK. |
error |
string (optional) |
Stringified exception, present only for error/warn levels. |
Clear the Delegate
AgentforceService.clearLoggerDelegate();
Platform Differences
| Platform | Log Levels Emitted |
|---|---|
| iOS | error, warn, info, debug |
| Android | error, warn, info |
iOS emits the debug level; Android doesn't. Plan your log filtering accordingly.
Example: Analytics Forwarding
AgentforceService.setLoggerDelegate({
onLog(level, message, error) {
if (level === "error") {
analytics.track("Agentforce Error", {
message,
error: error || undefined,
});
}
},
});
Navigation Delegate
The Navigation delegate lets your app handle navigation requests from the Agentforce SDK. When the agent presents a link, record reference, or other navigable element that the user taps, the SDK emits a navigation event.
Setup
import { Linking } from "react-native";
import { AgentforceService } from "@salesforce/react-native-agentforce";
AgentforceService.setNavigationDelegate({
onNavigate(request) {
switch (request.type) {
case "link":
if (request.uri) {
Linking.openURL(request.uri as string);
}
break;
case "record":
// Navigate to a record detail screen in your app
navigation.navigate("RecordDetail", {
recordId: request.recordId,
objectType: request.objectType,
});
break;
case "quickAction":
console.log(`Quick action requested: ${request.actionName}`);
break;
case "objectHome":
navigation.navigate("ObjectList", {
objectType: request.objectType,
});
break;
default:
console.log("Unhandled navigation:", JSON.stringify(request));
}
},
});
Navigation Request Types
| Type | Description | Fields |
|---|---|---|
record |
Navigate to a Salesforce record | recordId, objectType, pageReference? |
link |
Open an external or internal URL | uri, pageReference? |
quickAction |
Execute a Salesforce quick action | actionName, recordId?, objectType? |
pageReference |
Navigate to a Lightning page reference | pageReference |
objectHome |
Navigate to an object's home/list page | objectType, pageReference? |
app |
Navigate to an app or external package | packageName, uri? |
unknown |
Catch-all for unrecognized types | raw |
Clear the Delegate
AgentforceService.clearNavigationDelegate();
View Provider Delegate
The View Provider delegate allows you to replace native SDK output views with custom React Native components. When the SDK renders a component type that matches your registered map, your React Native component is rendered instead.
Setup
There are two parts to registering a custom view:
Part 1: Register the React Native component with AppRegistry
import { AppRegistry } from "react-native";
import { ViewProviderComponentData } from "@salesforce/react-native-agentforce";
function CustomRichTextView({ componentData }: { componentData: ViewProviderComponentData }) {
const text = componentData.properties.text as string;
return (
<View style={{ padding: 12 }}>
<Text style={{ fontSize: 16 }}>{text}</Text>
</View>
);
}
// Register it with AppRegistry
AppRegistry.registerComponent("CustomRichTextView", () => CustomRichTextView);
Part 2: Register the component map with the bridge
await AgentforceService.setViewProviderDelegate({
componentMap: {
"copilot/richText": "CustomRichTextView",
"copilot/markdown": "CustomMarkdownView",
},
});
The keys are SDK component definition strings. The values are the names you registered with AppRegistry.registerComponent().
Complete Custom View Provider Example
This example shows a full custom view component that handles all properties and sub-components:
// CustomAgentforceView.tsx
import React from "react";
import { View, Text, ScrollView, StyleSheet } from "react-native";
import type { ViewProviderComponentData } from "@salesforce/react-native-agentforce";
interface Props {
componentData: ViewProviderComponentData;
}
function CustomAgentforceView({ componentData }: Props) {
const { definition, name, properties, subComponents } = componentData;
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.defLabel}>{definition}</Text>
{name && <Text style={styles.nameLabel}>{name}</Text>}
</View>
<ScrollView style={styles.propsContainer}>
{Object.entries(properties).map(([key, value]) => (
<View key={key} style={styles.propRow}>
<Text style={styles.propKey}>{key}:</Text>
<Text style={styles.propValue}>
{typeof value === "object" ? JSON.stringify(value) : String(value)}
</Text>
</View>
))}
</ScrollView>
{subComponents && subComponents.length > 0 && (
<View style={styles.subSection}>
<Text style={styles.subTitle}>Sub-Components ({subComponents.length})</Text>
{subComponents.map((sub, index) => (
<View key={index} style={styles.subItem}>
<Text style={styles.subDef}>{sub.definition}</Text>
<Text style={styles.subProps}>{JSON.stringify(sub.properties, null, 2)}</Text>
</View>
))}
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
margin: 8,
padding: 12,
backgroundColor: "#f8f9fa",
borderRadius: 8,
borderWidth: 1,
borderColor: "#dee2e6",
},
header: { marginBottom: 8 },
defLabel: { fontSize: 14, fontWeight: "bold", color: "#0176D3" },
nameLabel: { fontSize: 12, color: "#666", marginTop: 2 },
propsContainer: { maxHeight: 200 },
propRow: { flexDirection: "row", marginBottom: 4 },
propKey: { fontWeight: "600", marginRight: 8, color: "#333" },
propValue: { flex: 1, color: "#555" },
subSection: { marginTop: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: "#eee" },
subTitle: { fontWeight: "bold", marginBottom: 4 },
subItem: { marginLeft: 12, marginBottom: 8 },
subDef: { fontWeight: "600", fontSize: 12 },
subProps: { fontFamily: "monospace", fontSize: 10, color: "#666" },
});
export default CustomAgentforceView;
Register the component and mapping in your App.tsx:
import { AppRegistry } from "react-native";
import CustomAgentforceView from "./CustomAgentforceView";
import { AgentforceService } from "@salesforce/react-native-agentforce";
// Register the component with AppRegistry -- this makes it available
// for the native bridge to render via RCTRootView / ReactRootView.
AppRegistry.registerComponent("CustomAgentforceView", () => CustomAgentforceView);
// Register the mapping with the bridge
async function setupCustomViews() {
await AgentforceService.setViewProviderDelegate({
componentMap: {
"copilot/richText": "CustomAgentforceView",
"copilot/markdown": "CustomAgentforceView",
"copilot/recordInfo": "CustomAgentforceView",
"copilot/list": "CustomAgentforceView",
},
});
}
// Call during app initialization
setupCustomViews();
Component Data Interface
interface ViewProviderComponentData {
definition: string; // Component definition (e.g. 'copilot/richText')
name?: string; // Component name from SDK (may be null)
properties: Record<string, unknown>; // Key-value properties
subComponents?: ViewProviderComponentData[]; // Nested sub-components
}
Known Component Definitions
copilot/listcopilot/recordInfocopilot/markdowncopilot/richText
How It Works
When you register a View Provider, the following flow occurs:
- You register a
componentMapmapping SDK definition strings to React Native component names. - The native bridge stores this map in
BridgeViewProvider. - When the SDK needs to render a view, it calls
canHandle(definition)on the view provider. - If the definition matches a key in the map, the provider returns
true. - The SDK then calls
view()(iOS) orGetView()(Android), which creates anRCTRootView(iOS) orReactRootView(Android). - Your React Native component is rendered, receiving
componentDataas its initial props containing thedefinition,name,properties, and anysubComponents.
Feature Flag Requirement
The enableCustomViewProvider feature flag must be true:
await AgentforceService.configure({
type: "service",
// ...
featureFlags: { enableCustomViewProvider: true },
});
Clear the Delegate
await AgentforceService.clearViewProviderDelegate();
Registration Timing
Register delegates before calling configure() to ensure they're active when the SDK initializes:
// 1. Set up delegates
AgentforceService.setLoggerDelegate({ onLog: handleLog });
AgentforceService.setNavigationDelegate({ onNavigate: handleNavigation });
await AgentforceService.setViewProviderDelegate({
componentMap: {
/* ... */
},
});
// 2. Configure
await AgentforceService.configure({
/* ... */
});
// 3. Launch
await AgentforceService.launchConversation();
Cleanup
Always clean up delegates when they're no longer needed:
// In a React component:
React.useEffect(() => {
AgentforceService.setLoggerDelegate({ onLog: handleLog });
AgentforceService.setNavigationDelegate({ onNavigate: handleNav });
return () => {
AgentforceService.clearLoggerDelegate();
AgentforceService.clearNavigationDelegate();
};
}, []);
Or call destroy() on app shutdown to clean up everything:
AgentforceService.destroy();