package com.vivocha.react import android.app.Activity import android.util.Log import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import com.vivocha.sdk.Vivocha import com.vivocha.sdk.VivochaContact import com.vivocha.sdk.VivochaValues import com.vivocha.sdk.model.VivochaConversation import com.vivocha.sdk.model.VivochaCustomAction import com.vivocha.sdk.model.chat.VivochaChatAck import com.vivocha.sdk.model.chat.VivochaMessage import com.vivocha.sdk.model.chat.VivochaPresence import org.json.JSONObject import java.util.UUID class VivochaModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), LifecycleEventListener { init { Log.i(TAG, "VivochaModule init") reactContext.addLifecycleEventListener(this) registerCallbacks() } private var events: MutableMap = mutableMapOf( "agentdata" to 0, "persistence" to 0, "agentpresence" to 0, "agenttyping" to 0, "chatackreceived" to 0, "chatacksent" to 0, "closeremote" to 0, "actionsent" to 0, "messagereceived" to 0, "messagesent" to 0, "attachmentreceived" to 0, "attachmentsent" to 0, "screenshotsession" to 0, "terminate" to 0, "transferred" to 0, "terminatebutton" to 0, "chatviewminimized" to 0, ) private var tappedUrls: MutableMap = mutableMapOf() private var acctId : String = "" private var servId : String = "" private var startOptions : ReadableMap? = null @Volatile private var pendingStart = false; companion object { const val NAME = "Vivocha" const val TAG = NAME } override fun getName(): String { return NAME } private fun emit(name: String, data: Any?) { this.reactApplicationContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(name, data?.let { Utils.jsonExport(it) }) } private fun registerCallbacks() { Vivocha.manager().setCallbacks( object: Vivocha.VivochaCallbacks { override fun onNewAgentData(available: Boolean, numberOfChats: Int, data: JSONObject?) { if (events["agentdata"]!! > 0) { emit("agentdata", JSONObject() .put("available", available) .put("numberOfChats", numberOfChats) .put("data", data) ) } } override fun onPersistence(conversation: VivochaConversation?, currentContact: VivochaContact?, didResumeContactFromConversation: Boolean) { if (events["persistence"]!! > 0) { emit("persistence", JSONObject() .put("conversation", conversation?.let { Utils.conversationToJson(it) }) .put("currentcontact", currentContact?.toCordovaJSON()) .put("didresumecontactfromconversation", didResumeContactFromConversation) ) } } } ) Vivocha.manager().setContactEventCallbacks( object: VivochaContact.VivochaContactEventCallbacks { override fun onAgentPresence(presence: VivochaPresence?) { if (events["agentpresence"]!! > 0) { emit("agentpresence", JSONObject() .put("presence", presence?.let { Utils.presenceToJson(it) }) ) } } override fun onMessageReceived(message: VivochaMessage?) { if (events["messagereceived"]!! > 0) { emit("messagereceived", JSONObject() .put("message", message?.let { Utils.messageToJson(it) }) ) } } override fun onChatAckReceived(ack: VivochaChatAck?) { if (events["chatackreceived"]!! > 0) { emit("chatackreceived", JSONObject() .put("ack", ack?.let {Utils.ackToJson(ack)}) ) } } override fun onMessageSent(message: VivochaMessage?) { if (events["messagesent"]!! > 0) { emit("messagesent", JSONObject() .put("message", message?.let {Utils.messageToJson(message)}) ) } } override fun onChatAckSent(ack: VivochaChatAck?) { if (events["chatacksent"]!! > 0) { emit("chatacksent", JSONObject() .put("ack", ack?.let {Utils.ackToJson(ack)}) ) } } override fun onCustomActionSent(action: VivochaCustomAction?) { if (events["actionsent"]!! > 0) { emit("actionsent", JSONObject() .put("name", action?.actionName) .put("id", action?.actionID) .put("data", action?.actionData) ) } } override fun onAgentTyping(isTyping: Boolean) { if (events["agenttyping"]!! > 0) { emit("agenttyping", JSONObject() .put("isTyping", isTyping) ) } } override fun onUserDidTerminatedChatView(contactId: String?) { if (events["terminatebutton"]!! > 0) { emit("terminatebutton", JSONObject() .put("contactId", contactId) ) } } override fun onChatViewMinimized() { if (events["chatviewminimized"]!! > 0) { emit("chatviewminimized", null) } } override fun onCloseFromOtherPeer(contactId: String?) { if (events["closeremote"]!! > 0) { emit("closeremote", JSONObject() .put("contactId", contactId) ) } } override fun onTerminate(contactId: String?) { if (events["terminate"]!! > 0) { emit("terminate", JSONObject() .put("contactId", contactId) ) } } override fun onTransferred(oldContactId: String?, newContactId: String?) { if (events["transferred"]!! > 0) { emit("transferred", JSONObject() .put("oldContactId", oldContactId) .put("newContactId", newContactId) ) } } override fun onScreenshotSessionChanged(authorized: Boolean) { if (events["screenshotsession"]!! > 0) { emit("screenshotsession", JSONObject() .put("authorized", authorized) ) } } } ) } @ReactMethod fun start(acctId: String, servId: String, options: ReadableMap?, promise: Promise) { this.acctId = acctId; this.servId = servId; this.startOptions = options; Vivocha.setStartCallback( object: Vivocha.VivochaStartCallback { override fun onStartSuccess(data: JSONObject?) { promise.resolve(Utils.jsonToMap(data)) } override fun onStartError( reason: Vivocha.VivochaStartCallback.VivochaStartErrorReason?, status: Int, data: JSONObject? ) { promise.reject(status.toString(), reason.toString()) } } ) val activity = this.reactApplicationContext.currentActivity if (activity != null){ Log.i(TAG, "Vivocha start - activity is not null") _startVivocha(activity); } else{ Log.i(TAG, "Vivocha start - activity is null, waiting for host resume") pendingStart = true; } } fun _startVivocha(activity: Activity) { Log.i(TAG, "Start Vivocha") Log.d(TAG, "Account ID: $acctId") Log.d(TAG, "Service ID: $servId") Log.d(TAG, "Options: $startOptions") Vivocha.start(activity, acctId, servId, Utils.mapToOptions(startOptions)) } @ReactMethod(isBlockingSynchronousMethod = true) fun stop(): Boolean { return Vivocha.stop(); } @ReactMethod(isBlockingSynchronousMethod = true) fun getDeveloperMode(): Boolean { return Vivocha.manager().developerMode; } @ReactMethod fun setDeveloperMode(mode: Boolean) { Vivocha.setDeveloperMode(mode) } @ReactMethod(isBlockingSynchronousMethod = true) fun getSideTab(): Boolean { return Vivocha.manager().isShowingSideTab; } @ReactMethod fun setSideTab(show: Boolean) { if (Vivocha.manager().isShowingSideTab != show) { if (show) { Vivocha.showSideTab() } else { Vivocha.hideSideTab() } } } @ReactMethod fun registerAction(code: String) { class CallBack : Vivocha.VivochaCustomActionCallback { override fun onAction(action: VivochaCustomAction) { emit("onAction", JSONObject() .put("code", code) .put("name", action.actionName) .put("id", action.actionID) .put("data", action.actionData) ) } } Vivocha.manager().bindCustomAction(code, CallBack()) } @ReactMethod fun setDataCollection(data: ReadableArray) { Log.i(TAG, "Set Data Collection") Log.d(TAG, "Data: $data") Vivocha.setDataCollection(Utils.arrayToDataCollection(data)) } @ReactMethod(isBlockingSynchronousMethod = true) fun getDataCollection(): WritableArray { return Utils.dataCollectionToArray(Vivocha.getDataCollection()) } @ReactMethod fun setCustomerToken(jwt: String) { Vivocha.setCustomerToken(jwt) } @ReactMethod fun unsetCustomerToken() { Vivocha.unsetCustomerToken() } @ReactMethod fun showView(animated: Boolean) { Vivocha.getContact()?.showView(animated) } @ReactMethod fun hideView(animated: Boolean) { Vivocha.getContact()?.hideView(animated) } @ReactMethod fun terminate(hideView: Boolean) { Vivocha.getContact()?.terminate(hideView) } @ReactMethod fun setPushRegistrationId(regId: String) { Vivocha.setPushRegistrationID(regId) } @ReactMethod fun registerEvent(event: String) { Log.i(TAG, "Register event $event") if (events.containsKey(event)) events[event] = events[event]!! + 1 } @ReactMethod fun unregisterEvent(event: String) { Log.i(TAG, "Unregister event $event") if (events[event]!! > 0) events[event] = events[event]!! - 1 } @ReactMethod fun createContact(data: ReadableArray, type: String, params: ReadableMap?, promise: Promise) { Log.i(TAG, "Create contact") Log.d(TAG, "Data: $data") Log.d(TAG, "Type: $type") Log.d(TAG, "Type: $params") Vivocha.createContact(Utils.arrayToDataCollection(data), type, Utils.mapToJson(params), object: Vivocha.VivochaContactCreationCallback { override fun onCreationSuccess(contact: VivochaContact?) { promise.resolve(Utils.jsonToMap(contact?.toCordovaJSON())) } override fun onCreationFailure() { promise.reject("ERROR", "Unable to create contact") } }) } @ReactMethod fun createChat(data: ReadableArray?, promise: Promise) { Log.i(TAG, "Create chat") Log.d(TAG, "Data: $data") Vivocha.createChat(Utils.arrayToDataCollection(data), object: Vivocha.VivochaContactCreationCallback { override fun onCreationSuccess(contact: VivochaContact?) { promise.resolve(Utils.jsonToMap(contact?.toCordovaJSON())) } override fun onCreationFailure() { promise.reject("ERROR", "Unable to create contact") } }) } @ReactMethod(isBlockingSynchronousMethod = true) fun getUnreadMessageCount(): Int { val contact = Vivocha.getContact() if (contact != null) { return contact.unreadMessagesCount } val conversation = Vivocha.getConversation() if (conversation != null) { return conversation.visitorUnreadMessagesForPersistenceWidget } return 0 } @ReactMethod(isBlockingSynchronousMethod = true) fun getContact(): WritableMap? { return Utils.jsonToMap(Vivocha.getContact()?.toCordovaJSON()) } @ReactMethod(isBlockingSynchronousMethod = true) fun getConversation(): WritableMap? { return Utils.jsonToMap(Vivocha.getConversation()?.json) } @ReactMethod fun stopScreenshotSession() { Vivocha.getContact()?.stopScreenshotSession() } @ReactMethod(isBlockingSynchronousMethod = true) fun isScreenshotSessionAuthorized(): Boolean { return Vivocha.getContact()?.hasUserAuthorizedScreenshotSession() == true } @ReactMethod fun setLanguage(language: String) { Vivocha.manager().overrideLanguage(language) } @ReactMethod(isBlockingSynchronousMethod = true) fun getAgent(): WritableMap? { return Utils.jsonToMap(Vivocha.getContact()?.agent?.let { Utils.agentToJson(it) }) } @ReactMethod(isBlockingSynchronousMethod = true) fun getVVCU(): String { return Vivocha.getVVCU() } @ReactMethod(isBlockingSynchronousMethod = true) fun getVVCT(): String { return Vivocha.getVVCT() } @ReactMethod fun storeSurvey(contactId: String, data: ReadableArray, promise: Promise) { Vivocha.storeSurvey( contactId, Utils.arrayToDataCollection(data) ) { returnContactId, success -> if (success) { promise.resolve(returnContactId) } else { promise.reject("ERROR", "Unable to store survey") } } } @ReactMethod fun createVideoChat(data: ReadableArray?, promise: Promise) { Vivocha.createVideoChat(Utils.arrayToDataCollection(data), object: Vivocha.VivochaContactCreationCallback { override fun onCreationSuccess(contact: VivochaContact?) { promise.resolve(Utils.jsonToMap(contact?.toCordovaJSON())) } override fun onCreationFailure() { promise.reject("ERROR", "Unable to create video chat") } } ) } @ReactMethod fun createCallBackNow(phoneNumber: String, data: ReadableArray?, promise: Promise) { Vivocha.createCallBackNow(Utils.arrayToDataCollection(data), phoneNumber, object: Vivocha.VivochaContactCreationCallback { override fun onCreationSuccess(contact: VivochaContact?) { promise.resolve(Utils.jsonToMap(contact?.toCordovaJSON())) } override fun onCreationFailure() { promise.reject("ERROR", "Unable to create callback now") } } ) } @ReactMethod fun addLocalization(language: String, localization: ReadableMap) { Vivocha.addLocalization(Utils.mapToLocalization(language, localization)) } @ReactMethod(isBlockingSynchronousMethod = true) fun getTheme(): WritableMap? { return Utils.themeToMap(Vivocha.getTheme()) } @ReactMethod fun setTheme(theme: ReadableMap) { Vivocha.setTheme(Utils.mapToTheme(theme)) } @ReactMethod(isBlockingSynchronousMethod = true) fun getMedia(): WritableMap? { return Vivocha.getContact()?.media?.let { Utils.mediaToMap(it) } } @ReactMethod fun sendAction(name: String, id: String, data: ReadableArray?) { Vivocha.getContact()?.sendAction(VivochaCustomAction(id, name, Utils.objToJson(data))) } @ReactMethod fun sendMessage(body: String, type: String?, payload: String?) { Vivocha.getContact()?.sendMessage(body, type, payload) } @ReactMethod fun sendAttachment(url: String, referenceId: String, title: String, mimetype: String, description: String, size: Int) { Vivocha.getContact()?.sendAttachment(url, referenceId, title, mimetype, description, size) } private fun waitForUrlTapped(id: String, timeout: Long): Boolean { val time = System.currentTimeMillis() while (tappedUrls[id] == null) { Thread.sleep(100) if (System.currentTimeMillis() - time >= timeout) { tappedUrls[id] = false } } val ret = tappedUrls[id] == true tappedUrls.remove(id) return ret } @ReactMethod() fun registerUrlTapped() { Vivocha.setTappedURLHandler { url -> val id: String = UUID.randomUUID().toString() emit( "urlTapped", JSONObject() .put("url", url) .put("id", id) ) waitForUrlTapped(id, 2000) } } @ReactMethod fun urlTapped(id: String, handled: Boolean) { tappedUrls[id] = handled } @ReactMethod(isBlockingSynchronousMethod = true) fun getSdkVersion(): String { return VivochaValues.VivochaSDKVersion } @ReactMethod(isBlockingSynchronousMethod = true) fun getSdkName(): String { return VivochaValues.VivochaSDKName } @ReactMethod(isBlockingSynchronousMethod = true) fun getSdkBaseUrl(): String { return VivochaValues.VivochaSDKBaseURL } @ReactMethod fun addListener(type: String?) { // Keep: Required for RN built in Event Emitter Calls. } @ReactMethod fun removeListeners(type: Int?) { // Keep: Required for RN built in Event Emitter Calls. } override fun onHostDestroy() { } override fun onHostPause() { } override fun onHostResume() { if (pendingStart) { val activity = this.reactApplicationContext.currentActivity Log.i(TAG, "Vivocha start - onHostResume activity is ${if (activity != null) "not" else "" } null") if (activity != null) { Log.i(TAG, "onHostResume (Vivocha) found pending start ") _startVivocha(activity) pendingStart = false } } } }