Skip to content

Commit 71a8800

Browse files
committed
@W-21447618: [React Native Bridge - iOS] Add hiddenPreChatFieldDelegate support
1 parent d687eaa commit 71a8800

File tree

10 files changed

+536
-2
lines changed

10 files changed

+536
-2
lines changed

AgentforceSDK-ReactNative-Bridge/android/src/main/java/com/salesforce/android/reactagentforce/AgentforceModule.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ class AgentforceModule(reactContext: ReactApplicationContext) :
7474
// Bridge view provider for delegating native SDK views to React Native components
7575
private val bridgeViewProvider = BridgeViewProvider(reactContext)
7676

77+
// Bridge hidden prechat fields (Service Agent only)
78+
private val bridgeHiddenPreChat = BridgeHiddenPreChat()
79+
7780
// Coroutine scope for async operations
7881
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
7982

@@ -616,6 +619,7 @@ class AgentforceModule(reactContext: ReactApplicationContext) :
616619
currentMode = null
617620
credentialProvider.reset()
618621
bridgeViewProvider.reset()
622+
bridgeHiddenPreChat.setFields(emptyMap())
619623
employeePrefs.edit().remove(KEY_EMPLOYEE_AGENT_ID).apply()
620624
promise.resolve(Arguments.createMap().apply {
621625
putBoolean("success", true)
@@ -627,6 +631,38 @@ class AgentforceModule(reactContext: ReactApplicationContext) :
627631
// region Event Emitter Support
628632
// endregion
629633

634+
// region Hidden PreChat Fields
635+
636+
/**
637+
* Pre-register hidden prechat field values for the next Service Agent session.
638+
*
639+
* TODO: Implement AgentforceHiddenPreChatFieldDelegate on BridgeHiddenPreChat
640+
* and pass to client.init(hiddenPreChatFieldDelegate = bridgeHiddenPreChat).
641+
* See iOS BridgeHiddenPreChat.swift for reference. Until then, fields are
642+
* stored but not sent to the SDK.
643+
*/
644+
@ReactMethod
645+
fun registerHiddenPreChatFields(fields: ReadableMap, promise: Promise) {
646+
val map = mutableMapOf<String, String>()
647+
val iterator = fields.keySetIterator()
648+
while (iterator.hasNextKey()) {
649+
val key = iterator.nextKey()
650+
fields.getString(key)?.let { map[key] = it }
651+
}
652+
bridgeHiddenPreChat.setFields(map)
653+
promise.resolve(null)
654+
}
655+
656+
@ReactMethod
657+
fun getHiddenPreChatFields(promise: Promise) {
658+
val result = Arguments.createMap()
659+
for ((key, value) in bridgeHiddenPreChat.getFields()) {
660+
result.putString(key, value)
661+
}
662+
promise.resolve(result)
663+
}
664+
// endregion
665+
630666
// region Additional Context
631667

632668
/**
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2026-present, salesforce.com, inc. All rights reserved.
3+
*
4+
* React Native bridge for hidden prechat field delegate
5+
*
6+
* TODO: Implement AgentforceHiddenPreChatFieldDelegate interface.
7+
* See BridgeHiddenPreChat.swift (iOS) for the reference implementation.
8+
*
9+
* Expected behavior:
10+
* - Store fields set via setFields()
11+
* - Implement AgentforceHiddenPreChatFieldDelegate.agentforce() to return
12+
* only values for fields the SDK requests (filter stored map by requested field names)
13+
* - Return null when no fields are stored or no requested fields match
14+
* - Use @Volatile for thread-safe reads (matches BridgeLogger pattern)
15+
*/
16+
package com.salesforce.android.reactagentforce
17+
18+
class BridgeHiddenPreChat {
19+
20+
@Volatile
21+
private var fields: Map<String, String> = emptyMap()
22+
23+
fun setFields(fields: Map<String, String>) {
24+
this.fields = fields
25+
}
26+
27+
fun getFields(): Map<String, String> = fields
28+
}

AgentforceSDK-ReactNative-Bridge/ios/Agentforce/AgentforceModule.m

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ @interface RCT_EXTERN_MODULE(AgentforceModule, RCTEventEmitter)
7171
RCT_EXTERN_METHOD(startNewConversation:(RCTPromiseResolveBlock)resolve
7272
rejecter:(RCTPromiseRejectBlock)reject)
7373

74+
// MARK: - Hidden PreChat Fields
75+
76+
RCT_EXTERN_METHOD(registerHiddenPreChatFields:(NSDictionary *)fields
77+
resolver:(RCTPromiseResolveBlock)resolve
78+
rejecter:(RCTPromiseRejectBlock)reject)
79+
80+
RCT_EXTERN_METHOD(getHiddenPreChatFields:(RCTPromiseResolveBlock)resolve
81+
rejecter:(RCTPromiseRejectBlock)reject)
82+
7483
// MARK: - Additional Context
7584

7685
RCT_EXTERN_METHOD(setAdditionalContext:(NSDictionary *)contextDict

AgentforceSDK-ReactNative-Bridge/ios/Agentforce/AgentforceModule.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ class AgentforceModule: RCTEventEmitter {
4242
private let listenerLock = NSLock()
4343
private var _hasListeners = false
4444

45+
// MARK: - Hidden PreChat Fields
46+
47+
/// Bridge delegate for hidden prechat fields (Service Agent only).
48+
/// Strongly retained here because AgentforceClient holds it as a weak reference.
49+
private let bridgeHiddenPreChat = BridgeHiddenPreChat()
50+
4551
// MARK: - Logging
4652

4753
/// Bridge logger for forwarding SDK logs to JavaScript.
@@ -180,6 +186,7 @@ class AgentforceModule: RCTEventEmitter {
180186
mode: .fullConfig(fullConfiguration),
181187
viewProvider: bridgeViewProvider
182188
)
189+
agentforceClient?.hiddenPreChatFieldDelegate = bridgeHiddenPreChat
183190
}
184191

185192
// MARK: - Employee Agent Configuration
@@ -813,6 +820,7 @@ class AgentforceModule: RCTEventEmitter {
813820
private func cleanupClient() {
814821
currentConversation = nil
815822
agentforceClient = nil
823+
bridgeHiddenPreChat.setFields([:])
816824
}
817825

818826
/// Close the conversation
@@ -829,6 +837,36 @@ class AgentforceModule: RCTEventEmitter {
829837
}
830838
}
831839

840+
// MARK: - Hidden PreChat Fields
841+
842+
/// Pre-register hidden prechat field values for the next Service Agent session.
843+
@objc
844+
func registerHiddenPreChatFields(
845+
_ fields: NSDictionary,
846+
resolver resolve: @escaping RCTPromiseResolveBlock,
847+
rejecter reject: @escaping RCTPromiseRejectBlock
848+
) {
849+
guard let dict = fields as? [String: String] else {
850+
reject(
851+
"INVALID_FIELDS",
852+
"Hidden prechat fields must be a map of string keys to string values",
853+
nil
854+
)
855+
return
856+
}
857+
bridgeHiddenPreChat.setFields(dict)
858+
resolve(nil)
859+
}
860+
861+
/// Get the currently registered hidden prechat field values.
862+
@objc
863+
func getHiddenPreChatFields(
864+
_ resolve: @escaping RCTPromiseResolveBlock,
865+
rejecter reject: @escaping RCTPromiseRejectBlock
866+
) {
867+
resolve(bridgeHiddenPreChat.getFields())
868+
}
869+
832870
// MARK: - Additional Context
833871

834872
/// Helper function to recursively convert Any value to JSEncodableValue
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (c) 2026-present, salesforce.com, inc. All rights reserved.
3+
*
4+
* React Native bridge for hidden prechat field delegate
5+
*/
6+
7+
import Foundation
8+
import AgentforceSDK
9+
import AgentforceService
10+
11+
/// Bridge delegate that returns pre-registered hidden prechat field values
12+
/// to the native Agentforce SDK during Service Agent session initialization.
13+
///
14+
/// JavaScript calls `registerHiddenPreChatFields({ key: value })` to store
15+
/// field values. When the SDK calls the delegate, only values for fields the
16+
/// SDK actually requests are returned; absent fields are omitted.
17+
///
18+
/// Must be strongly retained on the module because `AgentforceClient` holds
19+
/// `hiddenPreChatFieldDelegate` as a weak reference.
20+
class BridgeHiddenPreChat: NSObject, AgentforceHiddenPreChatFieldDelegate {
21+
22+
// MARK: - Properties
23+
24+
private let lock = NSLock()
25+
private var _fields: [String: String] = [:]
26+
27+
// MARK: - Public API
28+
29+
/// Replace the stored field values. Pass an empty dictionary to clear.
30+
func setFields(_ fields: [String: String]) {
31+
lock.withLock { _fields = fields }
32+
}
33+
34+
/// Return a snapshot of the current field values.
35+
func getFields() -> [String: String] {
36+
lock.withLock { _fields }
37+
}
38+
39+
// MARK: - AgentforceHiddenPreChatFieldDelegate
40+
41+
func agentforce(
42+
didRequestHiddenPreChatValues requestedFields: [AgentforceHiddenPreChatField]
43+
) async -> [String: String]? {
44+
let stored = lock.withLock { _fields }
45+
guard !stored.isEmpty else { return nil }
46+
47+
var result: [String: String] = [:]
48+
for field in requestedFields {
49+
if let value = stored[field.name] {
50+
result[field.name] = value
51+
}
52+
}
53+
return result.isEmpty ? nil : result
54+
}
55+
}

AgentforceSDK-ReactNative-Bridge/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export {
1717
} from './services/EmployeeAgentAuth';
1818
export type { AuthCredentials } from './services/EmployeeAgentAuth';
1919

20-
export type { ServiceAgentConfig, EmployeeAgentConfig, AgentConfig, FeatureFlags, LegacyServiceAgentConfig, ConfigurationResult, ConfigurationInfo, LoggerDelegate, LogLevel, NavigationDelegate, NavigationRequest, AgentforceAdditionalContext, AgentforceContextVariable, AgentforceContextVariableType, ViewProviderDelegate, ViewProviderComponentData } from './types';
20+
export type { ServiceAgentConfig, EmployeeAgentConfig, AgentConfig, FeatureFlags, LegacyServiceAgentConfig, ConfigurationResult, ConfigurationInfo, LoggerDelegate, LogLevel, NavigationDelegate, NavigationRequest, AgentforceAdditionalContext, AgentforceContextVariable, AgentforceContextVariableType, ViewProviderDelegate, ViewProviderComponentData, HiddenPreChatFields } from './types';
2121
export { isServiceAgentConfig, isEmployeeAgentConfig, isLegacyConfig } from './types';
2222

2323
export {

AgentforceSDK-ReactNative-Bridge/src/services/AgentforceService.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type {
3333
AgentforceContextVariableType,
3434
} from '../types/AgentforceContext';
3535
import { ViewProviderDelegate } from '../types/ViewProviderDelegate';
36+
import type { HiddenPreChatFields } from '../types/HiddenPreChatFields';
3637

3738
const { AgentforceModule } = NativeModules;
3839

@@ -42,6 +43,7 @@ export type { LoggerDelegate, LogLevel };
4243
export type { NavigationDelegate, NavigationRequest };
4344
export type { AgentforceAdditionalContext, AgentforceContextVariable };
4445
export type { ViewProviderDelegate };
46+
export type { HiddenPreChatFields };
4547

4648
/**
4749
* Native module event names
@@ -838,6 +840,86 @@ class AgentforceService {
838840
}
839841
}
840842

843+
/**
844+
* Pre-register hidden prechat field values for Service Agent conversations.
845+
*
846+
* Call this before `launchConversation()` to supply values for prechat fields
847+
* hidden from the end user (e.g. ContactId, AccountId, session tokens).
848+
* When the SDK initializes a Service Agent session, these values are returned
849+
* to the native delegate automatically.
850+
*
851+
* Fields not present in the provided map are omitted (not sent as empty strings),
852+
* so the SDK treats them as unset.
853+
*
854+
* @remarks Service Agent only. Has no effect for Employee Agent conversations.
855+
*
856+
* @param fields - Map of field developer names to string values
857+
*
858+
* @example
859+
* ```typescript
860+
* await AgentforceService.registerHiddenPreChatFields({
861+
* ContactId: '003xx0000001234',
862+
* AccountId: '001xx0000005678',
863+
* });
864+
* await AgentforceService.launchConversation();
865+
* ```
866+
*/
867+
async registerHiddenPreChatFields(fields: HiddenPreChatFields): Promise<void> {
868+
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
869+
return;
870+
}
871+
872+
if (!AgentforceModule) {
873+
console.warn('[AgentforceService] Native module not available');
874+
return;
875+
}
876+
877+
try {
878+
await AgentforceModule.registerHiddenPreChatFields(fields);
879+
const count = Object.keys(fields).length;
880+
console.log(
881+
`[AgentforceService] Hidden prechat fields registered: ${count} field(s)`,
882+
);
883+
} catch (error) {
884+
console.error(
885+
'[AgentforceService] Failed to register hidden prechat fields:',
886+
error,
887+
);
888+
throw error;
889+
}
890+
}
891+
892+
/**
893+
* Clear all pre-registered hidden prechat field values.
894+
*
895+
* @remarks Service Agent only.
896+
*/
897+
async clearHiddenPreChatFields(): Promise<void> {
898+
await this.registerHiddenPreChatFields({});
899+
}
900+
901+
/**
902+
* Get the currently registered hidden prechat field values.
903+
*
904+
* @returns The stored field map, or an empty object if none registered.
905+
*/
906+
async getHiddenPreChatFields(): Promise<HiddenPreChatFields> {
907+
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
908+
return {};
909+
}
910+
911+
if (!AgentforceModule?.getHiddenPreChatFields) {
912+
return {};
913+
}
914+
915+
try {
916+
const fields = await AgentforceModule.getHiddenPreChatFields();
917+
return (fields as HiddenPreChatFields) ?? {};
918+
} catch {
919+
return {};
920+
}
921+
}
922+
841923
/**
842924
* Reset all settings and clear the SDK state.
843925
*
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Hidden prechat field values for Service Agent conversations.
3+
*
4+
* Maps field developer names to their string values. These values are
5+
* submitted with the prechat form but are not visible to the end user.
6+
*
7+
* @remarks Service Agent only. Has no effect for Employee Agent conversations.
8+
*
9+
* @example
10+
* ```typescript
11+
* const fields: HiddenPreChatFields = {
12+
* ContactId: '003xx0000001234',
13+
* AccountId: '001xx0000005678',
14+
* Subject: 'Mobile App Support',
15+
* };
16+
* ```
17+
*/
18+
export type HiddenPreChatFields = Record<string, string>;

AgentforceSDK-ReactNative-Bridge/src/types/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ export {
3333

3434
// View provider delegate types
3535
export { ViewProviderDelegate, ViewProviderComponentData } from './ViewProviderDelegate';
36+
37+
// Hidden prechat field types
38+
export type { HiddenPreChatFields } from './HiddenPreChatFields';

0 commit comments

Comments
 (0)