Skip to content

Commit

Permalink
Merged PR 35304: Chat: haal remote configuratie op en verstuur pre ch…
Browse files Browse the repository at this point in the history
…at formulier

Related work items: #129471, #129472
  • Loading branch information
RikSchefferAmsterdam committed Oct 30, 2024
2 parents 069f751 + fbb81ad commit a81f491
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ import com.salesforce.android.smi.core.CoreClient
import com.salesforce.android.smi.core.CoreConfiguration
import com.salesforce.android.smi.core.data.domain.businessHours.BusinessHoursInfo
import com.salesforce.android.smi.core.data.domain.conversationEntry.entryPayload.event.typing.TypingIndicatorStatus
import com.salesforce.android.smi.core.data.domain.remoteConfiguration.DeploymentType
import com.salesforce.android.smi.core.data.domain.remoteConfiguration.PreChatConfiguration
import com.salesforce.android.smi.core.data.domain.remoteConfiguration.RemoteConfiguration
import com.salesforce.android.smi.core.events.CoreEvent
import com.salesforce.android.smi.network.data.domain.conversation.Conversation
import com.salesforce.android.smi.network.data.domain.conversationEntry.ConversationEntry
import com.salesforce.android.smi.network.data.domain.conversationEntry.entryPayload.EntryPayload
import com.salesforce.android.smi.network.data.domain.conversationEntry.entryPayload.event.entries.ParticipantClientMenu
Expand All @@ -33,6 +36,14 @@ import com.salesforce.android.smi.network.data.domain.conversationEntry.entryPay
import com.salesforce.android.smi.network.data.domain.conversationEntry.entryPayload.message.format.FormResponseFormat
import com.salesforce.android.smi.network.data.domain.conversationEntry.entryPayload.message.format.StaticContentFormat
import com.salesforce.android.smi.network.data.domain.participant.Participant
import com.salesforce.android.smi.network.data.domain.prechat.FormField
import com.salesforce.android.smi.network.data.domain.prechat.PreChatField
import com.salesforce.android.smi.network.data.domain.prechat.PreChatFieldType
import com.salesforce.android.smi.network.data.domain.prechat.choicelist.ChoiceList
import com.salesforce.android.smi.network.data.domain.prechat.choicelist.ChoiceListConfiguration
import com.salesforce.android.smi.network.data.domain.prechat.choicelist.ChoiceListValue
import com.salesforce.android.smi.network.data.domain.prechat.choicelist.ChoiceListValueDependency
import com.salesforce.android.smi.network.data.domain.prechat.termsAndConditions.TermsAndConditions
import com.salesforce.android.smi.network.internal.api.sse.ServerSentEvent
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
Expand All @@ -51,6 +62,7 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat
private var config: Configuration? = null
private var coreClient: CoreClient? = null
private var conversationClient: ConversationClient? = null
private var remoteConfiguration: RemoteConfiguration? = null
private var supervisorJob = SupervisorJob()
private var scope = MainScope()
// private var scope = CoroutineScope(Dispatchers.IO + this.supervisorJob)
Expand Down Expand Up @@ -112,13 +124,84 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat
}
scope.launch {
try {
// Since retrieveRemoteConfiguration is a suspend function, we can call it here
val remoteConfig: Result<RemoteConfiguration> =
coreClient?.retrieveRemoteConfiguration()
?: throw IllegalStateException("Failed to retrieve remote configuration")
// remoteConfig.data
if (remoteConfig is Result.Success) {
promise.resolve(remoteConfig.toString())
remoteConfiguration= remoteConfig.data
val remoteConfigMap = Arguments.createMap()
remoteConfigMap.putString("name", remoteConfig.data.name)
remoteConfigMap.putString("deploymentType", remoteConfig.data.deploymentType.toString())
remoteConfigMap.putDouble("timestamp", (remoteConfig.data.timestamp/1000).toDouble())
remoteConfigMap.putMap("choiceListConfiguration", Arguments.createMap().apply {
putArray(
"choiceLists",
Arguments.createArray().apply {
remoteConfig.data.choiceListConfiguration?.choiceList?.forEach { choice ->
val choiceMap = Arguments.createMap()
choiceMap.putString("identifier", choice.choiceListId)
choiceMap.putArray(
"values",
Arguments.createArray().apply {
choice.choiceListValues.forEach { value ->
val valueMap = Arguments.createMap()
valueMap.putString("label", value.label)
valueMap.putString("valueId", value.choiceListValueId)
valueMap.putString("valueName", value.choiceListValueName)
valueMap.putInt("order", value.order)
valueMap.putBoolean("isDefaultValue", value.isDefaultValue)
pushMap(valueMap)
}
}
)
pushMap(choiceMap)
}
}
)

putArray(
"valueDependencies",
Arguments.createArray().apply {
remoteConfig.data.choiceListConfiguration?.choiceListValueDependencies?.forEach { dependency ->
val dependencyMap = Arguments.createMap()
dependencyMap.putString("childId", dependency.childChoiceListValueId)
dependencyMap.putString("parentId", dependency.parentChoiceListValueId)
pushMap(dependencyMap)
}
}
)
})
remoteConfigMap.putArray("preChatConfiguration", Arguments.createArray().apply {
remoteConfig.data.forms.forEach { form ->
pushMap(
Arguments.createMap().apply {
putString("formType", form.formType.toString())
putArray(
"hiddenPreChatFields",
Arguments.createArray().apply {
form.hiddenFormFields.forEach { field ->
pushMap(convertPreChatFieldToMap(field))
}
}
)
putArray(
"preChatFields",
Arguments.createArray().apply {
form.formFields.forEach { field ->
pushMap(convertPreChatFieldToMap(field))
}
}
)
}
)
}
})
remoteConfigMap.putMap("termsAndConditions", Arguments.createMap().apply {
putString("label", remoteConfig.data.termsAndConditions?.label)
putBoolean("isEnabled", remoteConfig.data.termsAndConditions?.isTermsAndConditionsEnabled ?: false)
putBoolean("isRequired", remoteConfig.data.termsAndConditions?.isTermsAndConditionsRequired ?: false)
})
promise.resolve(remoteConfigMap)
} else {
promise.reject("Error", remoteConfig.toString())
}
Expand Down Expand Up @@ -206,6 +289,11 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat
}
}

@ReactMethod(isBlockingSynchronousMethod = true)
override fun generateUUID(): String {
return UUID.randomUUID().toString()
}

private fun parseRole(role: String?): String? {
return when (role) {
"EndUser" -> "USER"
Expand Down Expand Up @@ -244,13 +332,28 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat
return array
}

private fun convertPreChatFieldToMap(field: PreChatField): WritableMap {
return Arguments.createMap().apply {
putBoolean("editable", field.isEditable)
putBoolean("isHidden", field.isHidden)
putString("label", field.labels.display)
putInt("maxLength", field.maxLength)
putString("name", field.name)
putInt("order", field.order)
putBoolean("required", field.required)
putString("type", field.type.toString())
putString("errorType", field.errorType.toString())
putString("value", field.userInput)
}
}

private fun convertEntryToMap(entry: ConversationEntry): WritableMap {
val map = Arguments.createMap()
map.putString("entryId", entry.entryId)
map.putMap("sender", convertParticipantToMap(entry.sender))
map.putString("senderDisplayName", entry.senderDisplayName)
map.putString("messageType", entry.entryType.toString())
map.putInt("timestamp", entry.timestamp.toInt())
map.putDouble("timestamp", (entry.timestamp/1000).toDouble())
map.putString("conversationId", entry.conversationId.toString())
map.putString("status", entry.status.toString())
map.putString("format", entry.payload.entryType.toString())
Expand Down Expand Up @@ -442,11 +545,11 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat
}

is EntryPayload.TypingIndicatorPayload -> {
map.putInt("startedTimestamp", payload.startedTimestamp.toInt())
map.putDouble("startedTimestamp", (payload.startedTimestamp/1000).toDouble())
}

is EntryPayload.TypingStartedIndicatorPayload -> {
map.putInt("startedTimestamp", payload.timestamp.toInt())
map.putDouble("startedTimestamp", (payload.timestamp/1000).toDouble())
}

is EntryPayload.TypingStoppedIndicatorPayload -> {
Expand All @@ -470,7 +573,6 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat
}
scope.launch {
try {
// Since retrieveRemoteConfiguration is a suspend function, we can call it here
val result: Result<ConversationEntry> =
conversationClient?.sendMessage(message)
?: throw IllegalStateException("Failed to send message")
Expand All @@ -491,6 +593,53 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat
}
}


@ReactMethod
override fun submitRemoteConfiguration(remoteConfig: ReadableMap, createConversationOnSubmit: Boolean, promise: Promise) {
try {
if (conversationClient == null) {
promise.reject("Error", "conversationClient not created.")
return
}

scope.launch {
try {
remoteConfiguration?.forms?.forEach { form ->
form.formFields.forEach { field ->
val fieldName = field.name
if (remoteConfig.hasKey(fieldName)) {
remoteConfig.getArray("preChatConfiguration")?.getMap(0)?.getArray("preChatFields")?.toArrayList()?.forEach { filledField ->
filledField as ReadableMap
filledField.getString("name")?.let { name ->
if (name == fieldName) {
filledField.getString("value")?.let { value ->
field.userInput = value
}
}
}
}
}
}
}

val result: Result<Conversation> =
conversationClient?.submitRemoteConfiguration(remoteConfiguration!!, createConversationOnSubmit)
?: throw IllegalStateException("Failed to send message")
if (result is Result.Success) {
promise.resolve(true)
} else {
promise.reject("Error", result.toString())
}
} catch (e: Exception) {
promise.reject("Error", e.message, e)
}
}
} catch (e: Exception) {
// Catch any exception and reject the promise
promise.reject("Error", "An error occurred: ${e.message}", e)
}
}

@ReactMethod
override fun sendPDF(filePath: String, promise: Promise) {
try {
Expand Down Expand Up @@ -540,7 +689,6 @@ class SalesforceMessagingInAppModule internal constructor(context: ReactApplicat
FileOutputStream(tempFile).use { fos -> fos.write(decodedBytes) }
scope.launch {
try {
// Since retrieveRemoteConfiguration is a suspend function, we can call it here
val result: Result<ConversationEntry> =
conversationClient?.sendImage(tempFile)
?: throw IllegalStateException("Failed to send message")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ abstract class SalesforceMessagingInAppSpec internal constructor(context: ReactA
developerName: String,
promise: Promise,
)

abstract fun generateUUID(): String
abstract fun retrieveRemoteConfiguration(promise: Promise)
abstract fun submitRemoteConfiguration(remoteConfiguration: ReadableMap, createConversationOnSubmit: Boolean, promise: Promise)
abstract fun createConversationClient(clientID: String?, promise: Promise)
abstract fun sendMessage(message: String, promise: Promise)
abstract fun sendReply(choice: ReadableMap, promise: Promise)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ @implementation SalesforceMessagingInApp
SMICoreConfiguration *config;
id<SMICoreClient> coreClient;
id<SMIConversationClient> conversationClient;
id<SMIRemoteConfiguration> remoteConfiguration;
NSMutableArray<id<SMIChoice>> *receivedChoices;

RCT_EXPORT_METHOD(createCoreClient:(NSString *)url
Expand Down Expand Up @@ -148,6 +149,7 @@ @implementation SalesforceMessagingInApp
// Handle the error by rejecting the promise
reject(@"retrieve_remote_config_failed", @"Failed to retrieve remote configuration", error);
} else if (remoteConfig != nil) {
remoteConfiguration = remoteConfig;
// Manually construct a dictionary from the properties of remoteConfig
NSMutableDictionary *configData = [NSMutableDictionary dictionary];

Expand Down Expand Up @@ -283,6 +285,93 @@ @implementation SalesforceMessagingInApp
}];
}


RCT_EXPORT_METHOD(submitRemoteConfiguration:(NSDictionary *)remoteConfigurationDict
createConversationOnSubmit:(BOOL)createConversationOnSubmit
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
@try {
// Ensure that the conversation client has been initialized
if (conversationClient == nil) {
NSError *error = [NSError errorWithDomain:@"ConversationClient Not Initialized"
code:500
userInfo:@{NSLocalizedDescriptionKey: @"ConversationClient is not initialized."}];
reject(@"send_reply_exception", @"ConversationClient is not initialized", error);
return;
}

id<SMIRemoteConfiguration> remoteConfig = remoteConfiguration;

// Get the pre-chat fields from the remote config.
NSArray *preChatFields = remoteConfiguration.preChatConfiguration.firstObject.preChatFields;
NSArray *filledPreChatFields = remoteConfigurationDict[@"preChatConfiguration"][0][@"preChatFields"];
if (preChatFields != nil) {
// Set the pre-chat values.
for (id field in preChatFields) {
NSString *fieldName = [field valueForKey:@"name"]; // Get the name of the field.

// Ensure that fieldName is a valid string.
if (![fieldName isKindOfClass:[NSString class]]) {
continue; // Skip if fieldName isn't a valid string.
}

// Find the corresponding field in filledPreChatFields with the same name.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == %@", fieldName];
id matchingField = [[filledPreChatFields filteredArrayUsingPredicate:predicate] firstObject];

// If a matching field is found, set its value.
if (matchingField != nil) {
NSString *fieldValue = matchingField[@"value"];

// Ensure fieldValue is a valid string before setting.
if ([fieldValue isKindOfClass:[NSString class]]) {
// Use Key-Value Coding to set the value safely.
[field setValue:fieldValue forKey:@"value"];
}
}
}
}
// Get the pre-chat fields from the remote config.
NSArray *hiddenPreChatFields = remoteConfiguration.preChatConfiguration.firstObject.hiddenPreChatFields;
NSArray *filledHiddenPreChatFields = remoteConfigurationDict[@"preChatConfiguration"][0][@"hiddenPreChatFields"];
if (hiddenPreChatFields != nil) {
// Set the pre-chat values.
for (id field in hiddenPreChatFields) {
NSString *fieldName = [field valueForKey:@"name"]; // Get the name of the field.

// Ensure that fieldName is a valid string.
if (![fieldName isKindOfClass:[NSString class]]) {
continue; // Skip if fieldName isn't a valid string.
}

// Find the corresponding field in filledHiddenPreChatFields with the same name.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == %@", fieldName];
id matchingField = [[filledHiddenPreChatFields filteredArrayUsingPredicate:predicate] firstObject];

// If a matching field is found, set its value.
if (matchingField != nil) {
NSString *fieldValue = matchingField[@"value"];

// Ensure fieldValue is a valid string before setting.
if ([fieldValue isKindOfClass:[NSString class]]) {
// Use Key-Value Coding to set the value safely.
[field setValue:fieldValue forKey:@"value"];
}
}
}
}
[conversationClient submitRemoteConfiguration:remoteConfig createConversationOnSubmit:createConversationOnSubmit];
resolve(@(YES));
} @catch (NSException *exception) {
// Handle exceptions by rejecting the promise
NSError *error = [NSError errorWithDomain:@"sendReply Exception"
code:500
userInfo:@{NSLocalizedDescriptionKey: [exception reason]}];
reject(@"send_reply_exception", @"An exception occurred during sendReply", error);
}
}

RCT_EXPORT_METHOD(sendMessage:(NSString *)message
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
Expand Down Expand Up @@ -455,6 +544,13 @@ @implementation SalesforceMessagingInApp
}
}

RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(generateUUID)
{
NSUUID *uuid;
uuid = [NSUUID UUID];
return [uuid UUIDString];
}

- (NSDictionary *)parseChoiceToDictionary:(id<SMIChoice>)choice
{
[receivedChoices addObject:choice];
Expand Down
Loading

0 comments on commit a81f491

Please sign in to comment.