Integrating IBM Verse with Salesforce and Lightning Out

In this blog post, we will take a look at how you can import data to Salesforce from IBM Verse via Lightning Components and Lightning Out.

The User Story

Ed is a sales representative who uses IBM Verse as his primary email system. Ed can also email his customers from Salesforce and have the email activity automatically logged to contact and account records in Salesforce – but emails sent or received directly within IBM Verse would not be captured. Valuable information – in his Verse inbox – is not visible to Ed or his colleagues from the CRM, and Ed needs this problem to be solved.

Identifying the Building Blocks

IBM Verse is a browser-based email application, which allows you to export user or email data.  For this particular use case we need email data to help Ed, and the data will get exported in JSON format to a new browser window using the Window.postMessage() method.

Also for every email that gets exported, we have to create a new activity and relate it to an account.

From Ed’s user perspective the workflow should be:

  • Open an email in Verse
  • Select an action to log the email
  • Select the account associated with the email recipient
  • Log the email activity

We decide to use the following setup:

  • IBM Verse sends the postMessage data to a node.js App on Heroku.
  • The node.js App will take the data from IBM Verse and also render a custom Lightning Component (via Lightning Out) for account selection.
  • The Lightning Component will create the email activity using Apex.

Now that you understand the architecture and use-case, let’s take a look at how you would set this up in detail.

IBM Verse

Verse allows developers to extend its web application interface with custom menu entries. For this use-case, we’re adding a “Log to Salesforce” action, so that Ed can easily log emails. Note that this kind of extension is currently only available in the cloud version of Verse.

The following example payload showcases how we set up the Verse configuration:

  {
    "app_id": "com.winkelmeyer.salesforce",
    "name": "Salesforce Verse integration",
    "url": "https://salesforce-ibm-verse.herokuapp.com/verse",

    "extensions": [
      {
        "type": "com.ibm.verse.action",
        "ext_id": "com.winkelmeyer.salesforce.logMail",
        "name": "Log to Salesforce",
        "payload": {},
        "path": "mail.read",
        "title": "Log to Salesforce"
      }
    ],
    "payload": {
      "features": [
        "core"
      ],
      "renderParams": {
        "width": "900",
        "height": "500"
      }
    },
    "services": [
      "Verse"
    ]
  }

You can find a detailed description of the available options for extending the Verse UI here. For our example here I want to highlight the URL parameter (line 4) which reflects the target URL of the new browser window for the Verse data.

Heroku node.js App

The app on Heroku uses node.js to render a simple HTML page which is used to receive the Verse data.

HTML Header (excerpt)

<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title><%= title %></title>
        <link rel="stylesheet" type="text/css" href="/assets/styles/salesforce-lightning-design-system.css" />
            <script>
        window.addEventListener("message", function(event) {
        if (event.origin !== "<%= verseSourceServer %>"){  // 
          return;
        }
        _verseObject = event.data;
        if (_verseObject.verseApiType === "com.ibm.verse.ping.application.loaded") {
          var loaded_message = {
            verseApiType : "com.ibm.verse.application.loaded"
          };
          event.source.postMessage(loaded_message, event.origin);
        }
      }, false);
    </script>
    </head>
    <body>

Code highlights:

  • Line 8: We’re checking the source URL of the server to prevent cross-site scripting attacks. The parameter <%= verseSourceServer %> refers to a variable that we’re getting from the Heroku environment variables.
  • Line 11: The Verse data is stored in the JavaScript variable _verseObject, so that we can access it in our JavaScript.

app.js (excerpt)

After the user has authenticated against the Salesforce org, the Lightning Out component is rendered using the setupLightning function.

function setupLightning(callback) {
  var lightningApp = config.lightningApp;
    var lightningComponent = config.lightningComponent;
    var oauth = force.getOauth();
    if (!oauth) {
        alert("Please login to Salesforce.com first!");
        return;
    }

    if (_lightningReady) {
        if (typeof callback === "function") {
            callback();
        }
    } else {
        // Transform the URL for Lightning
        var url = oauth.instanceUrl.replace("my.salesforce", "lightning.force");
        console.log(_verseObject);
        $Lightning.use(lightningApp,
            function() {
                 _lightningReady = true;
                 document.getElementById("oauth").style.display = "none";
                 $Lightning.createComponent(lightningComponent, { verseObject: _verseObject }, "component");
            }, url, oauth.access_token);
    }
}

Code Highlight:

  • Line 22: This is where the custom Lightning Component gets created. The JavaScript variable _verseObject, which holds the email data gets passed as a parameter to an identically-named aura attribute in the Lightning Component.

Lightning Out

Using Lightning Components via Lightning Out makes sense from multiple perspectives. First, we can use the existing technology stack like Lightning, Apex and more directly on the Force.com platform. And second, we can re-use this component in any of our Lightning apps.

In our example, Ed needs to see the subject of the email before logging it. And he needs to be able to search for accounts so that he can easily store the email activity in the right place. This is how Ed would like the interface to look:

To render a Lightning Component via Lightning Out we need to create a simple Lightning App that will act as a wrapper.

<aura:application extends="ltng:outApp">
    <aura:dependency resource="c:IBM_Verse_Mail_Retriever"/>
</aura:application>
  • The property extends="ltng:outApp" does two things: it declares this app as a Lightning Out app and automatically adds the Salesforce Lightning Design System (SLDS) to the app. The latter means that we don’t have to add static resources to the Salesforce org.
  • As a dependency to this Lightning app, we’ve to add the needed Lightning component.

To make life easier we decide to use Base Lightning Components, introduced in Winter ’17, within our Account Selection Lightning Component whenever possible. Using Base Lightning Components helps us reduce maintenance – and also the amount of code we need to write.

Component Markup

<aura:component implements="force:appHostable" controller="IBMVerseController">
    
    <aura:attribute name="accountSelected" type="Account" />
    <aura:attribute name="accounts" type="Account[]" />
    <aura:attribute name="verseObject" type="Object" />
    
    <div aura:id="lookupForm" 
         class="slds-form-element slds-lookup" 
         data-select="single">
        
        // Read only input field that displays the email subject based on the passed JSON data
        <lightning:input name="mailSubject"
                         label="Mail Subject"
                         value="{!v.verseObject.verseApiData.context.subject}"
                         disabled="true" />
        
        <br />
        
        // Read only field with built-in input validation
        <lightning:input aura:id="inputSearch"
                         name="inputSearch"
                         label="Account Search"
                         placeholder="Enter the Account name here"
                         minlength="3"
                         messageWhenBadInput="Your entry must be at least 3 characters."
                         onchange="{!c.onSearchKeyChange}" />
        
        // Building the lookup list for the search parameters in the inputSearch field
        <div class="slds-lookup__menu">
            <ul class="slds-lookup__list" role="listbox">
                <aura:iteration items="{!v.accounts}"
                                var="account"
                                indexVar="accountIndex">
                    <li role="presentation">
                        <span class="slds-lookup__item-action slds-media slds-media--left" role="option">
                            <lightning:icon iconName="standard:account"
                                            size="small"
                                            class="slds-icon slds-icon-standard-account slds-icon--small slds-media__figure"/>
                            <div class="slds-media__body">
                                <div class="slds-lookup__result-text"
                                     onclick="{!c.onAccountSelected}" 
                                     id="{!accountIndex}">{!account.Name}</div>
                            </div>
                        </span>
                    </li>
                </aura:iteration>
            </ul>
        </div>
        
        <br /><br />
        
        <lightning:button aura:id="btnLog" 
                          label="Log Mail" 
                          onclick="{!c.onLogMail}" 
                          disabled="true" />
        
    </div>
    
</aura:component>

Code highlights:

  • The lightning:xxx components (known as Base Lightning Components) give us elements that are automatically with SLDS.
  • Line 5: The aura attribute verseObject holds the Verse email data
  • Line 24: The minlength property in the inputSearch field adds automatic field validation.
  • Line 33/42: Using the indexVar property of aura:iteration as value for the id allows us to access the index value of the accounts aura:attribute.

Controller

({
    onSearchKeyChange : function(cmp, event, helper) {
        helper.searchKeyChange(cmp, event);
    },
    onAccountSelected : function(cmp, event, helper) {
        helper.accountSelected(cmp, event);        
    },
    onLogMail : function(cmp, event, helper) {
        helper.logMail(cmp, event);
    }
})

Helper

({
    searchKeyChange: function (cmp, event) {
        var action = cmp.get('c.findByName');
        var input = cmp.find('inputSearch');
        if (input.get('v.value')!=('') && !input.get('v.validity').tooShort) {
            action.setParams({ "searchKey": input.get('v.value') });
            action.setCallback(this, function(a) {
                cmp.set('v.accounts', a.getReturnValue());
                var form = cmp.find('lookupForm');
                $A.util.addClass(form, 'slds-is-open');
            });
            $A.enqueueAction(action);   
        } else {
            this.resetInputs(cmp);
        }
    },
    accountSelected : function(cmp, event) {
        var accounts = cmp.get('v.accounts');
        var account = cmp.get('v.accountSelected');
        var input = cmp.find('inputSearch');
        input.set('v.value', accounts[event.target.id].Name);
        cmp.set('v.accountSelected', accounts[event.target.id]);
        this.enableInputs(cmp);
    },
    logMail : function(cmp, event) {
        var action = cmp.get('c.createMailForVerse');
        var jsonObject = cmp.get('v.verseObject');
        var account = cmp.get('v.accountSelected');
        console.log(account.Id);
        action.setParams({ 'jsonObject': JSON.stringify(jsonObject), 'accountId': account.Id });
        action.setCallback(this, function(a) {
            window.self.close();
            alert('Message has been logged');
        });
        $A.enqueueAction(action);
    },
    resetInputs: function(cmp) {
        var button = cmp.find('btnLog');
        button.set('v.disabled', true);
        cmp.set('v.accounts', null);
        var form = cmp.find('lookupForm');
        $A.util.removeClass(form, 'slds-is-open');
    },
    enableInputs: function(cmp) {
        var button = cmp.find('btnLog');
        button.set('v.disabled', false);     
        cmp.set('v.accounts', null);
        var form = cmp.find('lookupForm');
        $A.util.removeClass(form, 'slds-is-open');
    }
})

Code highlights:

  • Line 5: !input.get('v.validity').tooShort checks the configured minlength value.
  • Line 21: With event.target.id we’re accessing the id property of the selected account from the lookup list.
  • Line 30: We’re passing the verseObject (the email data) as stringified JSON to the Apex callback method.

APEX CONTROLLER (excerpt)

As defined in the user story, Ed and his colleagues should see the imported email in the Activity stream for the selected account. To associate email data with the account, we’ll use the standard Salesforce EMailMessage object.

 @AuraEnabled    
 public static Boolean createMailForVerse(String jsonObject, String accountId) {
        
     Boolean success = false;
        
     IBMVerseMail verseMail = convertJsonToIBMVerseMail(jsonObject);
        
     if (verseMail!=null) {
         EmailMessage message = new EmailMessage();
         message.FromAddress = verseMail.sender.emailAddress;
         message.FromName = verseMail.sender.displayName;
            
         if (verseMail.recipientTo != null) {
               
             String[] recipients = new String[]{};
                 
                 for (IBMVerseMailAdressData addressData : verseMail.recipientTo) {
                     recipients.add(addressData.emailAddress);
                 }
                
             message.ToAddress = String.join(recipients, ',');    
         }
            
         if (verseMail.recipientCc != null) {
                
             String[] recipientsCc = new String[]{};
                  
                 for (IBMVerseMailAdressData addressData : verseMail.recipientCc) {
                     recipientsCc.add(addressData.emailAddress);
                 }
                
             message.CcAddress = String.join(recipientsCc, ',');            
         }
            
         message.MessageDate = verseMail.timeSent;
         message.Status = '3';
         message.HtmlBody = verseMail.body;
         message.Incoming = true;
         message.Subject = verseMail.subject;
         message.RelatedToId = accountId;
         insert message;
         
         success = true;
     }
        
     return success;
 }
    

In line 6 a helper method is used to convert the JSON string to an Apex object (IBMVerseMail) via the JSONParse.readValueAs(apex) method.

Get the Code on GitHub

As you can see, you can easily connect enterprise systems using the Force.com platform and Heroku, and by re-using Lightning Components in non-Salesforce applications via Lightning Out you can streamline your development efforts. Check out the complete code, including a video walkthrough and the option to install an unmanaged package, on GitHub.

About the Author

René Winkelmeyer works as a Senior Developer Evangelist in the Developer Relations team at Salesforce. He focuses on integrations, mobile, and security with the Force.com platform. You can follow him on Twitter on his handle @muenzpraeger.

Bookmark the permalink. Trackbacks are closed, but you can post a comment.
  • Mohammed Riaz

    Very Nice use of Lightning out , more doors are opening throught this channel.