Native mobile app for Salesforce Communities - Part 4 (Integration)

Gist: We now dive into the nuts and bolts of interacting with Salesforce 'Communities' using REST API and integrating with our custom mobile app built with Titanium. This final post of the series builds upon the overview, authentication and custom UI posts.

Armed with a custom UI mobile app and an Access token, we are now ready to call Salesforce's REST API and exchange data.

Since Salesforce exposes its standard objects via REST API, integration is easy and requires hardly any customization on the Server side.

Let's look at how our mobile app can query and post Case data to a Salesforce 'Communities'.

Salesforce 'Communities ' Case data in a TableView

The UI is actually pretty simple. It just has a TableView that is populated via code when user is navigated to this view. Here is the XML markup for the UI above.

<Alloy>
<Window class="container">
<ImageView id="image" image="/images/BestTech_Header_2.png" top="0" right="0"/>
<Label id="lblBack" top="0%" onClick="Click_back" opacity=1></Label>
<TableView id="tableCases" top="16%" backgroundColor="black"
separatorColor="#444"
onClick="openDetail">
</TableView>
</Window>
</Alloy>

So how does this work? The magic is in the 'get_CaseData' function which is invoked when the user clicks the 'View case' label on the main page.

This function makes an HTTP call to Salesforce' REST API, sends the SOQL, receives the JSON response, parses data, creates mobile UI, populates it with the parsed data and displays it to the user.

Clients making HTTP calls to 'Communities' are required to call their Communities instance specific URL (so, na15.salesforce.com) instead of the regular 'login.salesforce.com' that is used for Platform user.

Also, since REST is fundamentally stateless, the Access token is our only immediate way to handshake with the Salesforce server. We set the Access token and content type (to JSON) as part of the request header.

In the mobile app's code, we are using Titanium's Network.HTTPClient library and make HTTP GET calls to query data (and POST calls to insert data).

Finally, the SOQL query is set in the GET request and if everything is setup correctly, Salesforce will return the data in JSON format.

Usually, all this can happen in a fraction of a second and appear almost instant to the user (of course, depending on the connection, network bandwidth and the mobile hardware).

function get_CaseData() {

var loader = Alloy.Globals.loader;

//This code pulls data from a custom REST webservice.

loader.open('GET', 'https://na13.salesforce.com' +
'/services/data/v28.0/query?q=' +
Ti.Network.encodeURIComponent('SELECT Id,CaseNumber, Subject, Status, LastModifiedDate, CreatedDate FROM Case ORDER BY CreatedDate DESC'));
//Hardcoded instance URL...We should be retrieving these from a db adapter or from a Global property setting

loader.setRequestHeader("Authorization", "OAuth " + Alloy.Globals.strAccessToken);
//Access token should also come from the Prototype db adapter instead of a Global variable

loader.setRequestHeader("Content-Type", "application/json");
loader.send();


loader.onload = function () {

data = JSON.parse(this.responseText);

//The data retrieved here should be stored using a db adapter / Case model
var rows = [];

for (var i = 0, l = data.records.length; i < l; i++) {

var rec = data.records[i];

var row = Ti.UI.createTableViewRow({
id: rec.Id,
caseNumber: data.records[i].CaseNumber,
status: data.records[i].Status,
lastModifiedDate: data.records[i].LastModifiedDate,
createdDate: data.records[i].CreatedDate
});

var labelTitle = Ti.UI.createLabel({
text: rec.Subject,
font: {
fontSize: '24dp'
},
color: '#fff',
left: 0,
top: 0,
textAlign: 'left',
verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_CENTER
});
row.add(labelTitle);
rows.push(row);
}
$.tableCases.setData(rows);
};

loader.onerror = function (e) {
// this function is called when an error occurs, including a timeout
Ti.API.debug(e.error);
alert('Error: We were unable to retrieve the data :-(. Please try again.');
};
};

Ti.API.info("invoking get / REST API call");
get_CaseData();

//Back button...This can also use the iOS navigation group...
function Click_back(e) {
$.caseView.close();
};

$.lblBack.text = 'Back';

//Open the detail view
function openDetail(e) {
Alloy.createController('caseDetail').getView().open(e);
};

Once the JSON is received, the mobile app parses the JSON data into an array, populates the TableViewRow (one element at a time), adds them to the TableView, and displays it to the user.

The TableView also offers a 'click' event that can be used to create a List:Detail type functionality. We can catch the 'click' event to get the active row, and show a detail view, or open it in edit mode etc.

Now, let's take a look at how we can create Case records with our rather simplistic 'New Case' UI.

Once the user has populated the UI with case data and clicked submit, we need to submit this data to Salesforce and create the case record in our Communities instance. Here is the markup for the UI above.

<Alloy>
<Window class="container">
<ImageView id="image" image="/images/BestTech_Header_2.png" top="0" right="0" />
<Label id="lblBack" top="0%"
onClick="Click_back" opacity=1></Label>
<TextField id="txtCaseSubject" top="16%" width="85%" height="6%"></TextField>
<TextArea id="txtCaseDescription" top="25%" width="85%" height="60%"></TextArea>
<Label id="lblCaseOpen" top="87%" left="15%" right="15%"
textAlign="Ti.UI.TEXT_ALIGNMENT_CENTER"
onClick="Click_open">
Submit Case
</Label>
</Window>
</Alloy>

Most of the steps are fairly similar (setting up request headers for Access Token and Content-Type).

The key difference is that we are now making a POST request and so our app has to parse the UI values and creates a syntactically correct JSON string for submission to Salesforce.

Here's the code for that...

function Case_open(e) {

Ti.API.info("Case Open clicked " + $.txtCaseSubject.value + " - " + $.txtCaseDescription.value);

var txtSendCaseData = '{ "Subject" : "' + $.txtCaseSubject.value + '", "Description" : "' + $.txtCaseDescription.value + '"}';

Ti.API.info('Parsed text for post looks like this...' + txtSendCaseData);

//loader is a global instance of the Titanium.Network.HTTPclient
var loader = Alloy.Globals.loader;

//Notice that we use an instance specific URL here (instead of a login.salesforce.com one)
loader.open('POST', 'https://na13.salesforce.com' + '/services/data/v28.0/sobjects/Case');

//Access token is retrieved from a global property. It can also be stored in a Prototype db adapter instead
loader.setRequestHeader("Authorization", "OAuth " + Alloy.Globals.strAccessToken);

//Setting additional parameters to let server know what kind of data is sent
loader.setRequestHeader("Content-Type", "application/json");

//Setting the JSON formatted data to be posted
loader.send(txtSendCaseData);

loader.onload = function () {
Titanium.API.info('Status: ' + this.status);
Titanium.API.info('ResponseText: ' + this.responseText);
Titanium.API.info('connectionType: ' + this.connectionType);
Titanium.API.info('location: ' + this.location);

if (this.status == 201) {
alert('Your message was posted successfully!');
} else {
alert('Ooops...we broke something :-(');
}
};

loader.onerror = function (e) {

// this function is called when an error or timeout occurs.
Ti.API.debug(e.error);
alert('Error: We were unable to post your message :-(. Please try again.');
};
};

If all goes well, Salesforce will create a new case and respond with an HTTP status of '201' and send the Case Id in the responseText. Here is a sample output from Titanium's debug console...

[INFO] :   Status: 201
[INFO] : ResponseText: {"id":"500a000000eI6JQAA0","success":true,"errors":[]}
[INFO] : connectionType: POST
[INFO] : location: https://na13.salesforce.com/services/data/v28.0/sobjects/Case

Summary: In this series of posts, we saw how Salesforce's REST API for 'Communities' allows for integrating with custom mobile app platforms, and can enable us to create custom mobile experiences with relatively limited technical effort.

Imagine building upon mobile's native capabilities with Camera, Geo and Gyro sensors and combining it with the best of the cloud. We have barely scratched the surface of something much bigger.