Pebble Watch: Salesforce Approvals – Part 2

Watchface

In Part 1, we looked at creating the Salesforce server application to expose the Approvals and the target objects via some REST based web services.

In the second part of this article we look at creating the Pebble watch app and hooking it up the web services.

Creating a connected Application

The first thing we need to do is create a connected application in Salesforce.

To do this, login to Salesforce and navigate to:

Setup->Create->Apps->Connected Apps and create a new App.

Give it an App Name, API Name and Contact Email.

Check the Enable OAuth Settings and enter a Callback URL of sfdc://success and finally make sure Access and manage your data (api) is in the Selected OAuth Scopes. Since you will be connecting via your mobile phone you should also set IP Relaxation to Relax IP restrictions.

Save your changes.

You’re going to need 2 pieces of information from the Connected App, the Consumer Key and Consumer Secret. For now just remember where to find them as you’ll need them later.

Creating the Pebble Watch App

If you havn’t already got one, you’ll need to sign up for a Pebble Cloud Developer Account at: https://cloudpebble.net/

Create a new Project of type: Pebble.js

You’re going to need a bunch of images. You’ll find all these in the GitHub at https://github.com/tscottdev/PebbleSFDCApprovals/tree/master/resources/images

Click the Add New button next to the Project Resources and upload them.

Next we’ll need to configure the Project a little. Click on the Settings menu.

Make sure the App Kind is set to Watchapp, the Menu Image is checkbox28x28.png and the Configurable checkbox is checked.

Adding the Configuration

In order to supply user credentials to Salesforce we are going to make the app configurable. This allows the user to configure the application via the Pebble companion app:

Pebble Companion App Pebble Configuration

The user clicks the gear icon in the companion app and is presented with a configuration page. The configuration page is an html page hosted on your own web server. For this exercise I’ve hosted the html page in the public folder of my dropbox account and used the public URL to access it.

The html configuration page looks like this:

<!DOCTYPE html>
<html>
<head>
	<title>SFDC Approvals - Configuration</title>
</head>

<style>
	body {
		font-size: 12pt;
		color: white;
		background-color: black;
	}
	h1 {
		font-size: 14pt;
	}
	input {
		width: 100%;
	}
</style>

<script>
	// Get the Query Parameter
	function getQueryParam(variable, default_)
	{
		var query = location.search.substring(1);

		var vars = query.split('&');

		for (var i = 0; i < vars.length; i++)
		{
			var pair = vars[i].split('=');

			if (pair[0] == variable)
			{
				return decodeURIComponent(pair[1]);
			}
		}

		return default_ || false;
	}

	function save()
	{
		var unElm = document.getElementById('un');
		var pwElm = document.getElementById('pw');

		var configuration = { un: unElm.value, pw: pwElm.value };

		// Close config page and return data
		var return_to = getQueryParam('return_to', 'pebblejs://close#');
		location.href = return_to + encodeURIComponent(JSON.stringify(configuration));
	}
</script>

<body>
	<h1>SFDC Approvals - Configuration</h1>

	<form>
		User name:<br>
		<input type="text" name="un" id="un">
		<br>
		User password:<br>
		<input type="password" name="pw" id="pw">
		<br>
		<button type="button" onclick="save();">Save</button>
	</form>

</body>

</html>

The configuration page prompts for a username and password and passes it back to a local address as a JSON string. There’s some code in there to handle the call back address for the emulator as well as the companion app. We’ll see how we hook this up when we get onto the watch code.

Pebble Watch Code

First up we’ll create a new Javascript file called app-settings.js from within the CloudPebble IDE. We’ll use this to store a few constants, including the Salesforce App Client Id (Consumer Key) and Client Secret (Consumer Secret) from earlier. The main app will add an include to this file.

You’ll need to modify some of these values to suit your org:

/**
 * App Settings - You will need to replace this with values applicable to your org
 */
var appSettings = {
    // URL to the page hosting the Apps Config Screen
    configURL: 'https://dl.dropboxusercontent.com/u/XXXXXX/PebbleApps/SFDCApprovals/config-page.html',

    // The Client Id and Secret of your Connected App
    clientId: 'XXXXXXX.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXX.XXXXXXXXXXXXXXXX.XXXXXXXX',
    clientSecret: 'XXXXXXXXXXXXXXXXXXX',

    // The URL prefix for the REST bases service calls, amend your instance and namespace accordingly
    serviceURLprefix: 'https://na10.salesforce.com/services/apexrest/mynamespace/'
};

this.exports = appSettings;

We’ll now create another Javascript file in the CloudPebble IDE called app.js. This is the main application code:

/**
 * (c) Tony Scott. Licensed under a Creative Commons Attribution 3.0 Unported License.
 * http://creativecommons.org/licenses/by/3.0/deed.en_US
 *
 * This software is provided as is, at your own risk and without warranty of any kind.
 *
 * Please visit my blog: https://meltedwires.com/ and 'like' if you've found this useful.
 */

// Required Libraries
var UI = require('ui');
var ajax = require('ajax');
var Vector2 = require('vector2');
var Settings = require('settings');
var AppSettings = require('app-settings');

// Salesforce Token (returned by the login function)
var token = '';

// Main Card
var main = new UI.Card({
    title: 'SFDC',
    subtitle: 'Approvals',
    icon: 'images/checkbox28x28.png',
    body: 'Connecting ...',
    action: { select: 'images/reload16x16.png' }
});

// Add Event listners for Configuration Screen to get and store the User and Password
Pebble.addEventListener('showConfiguration', function(e) {
    // Show config page
    console.log('AppSettings.configURL=' + AppSettings.configURL);
    Pebble.openURL(AppSettings.configURL);
});

Pebble.addEventListener('webviewclosed', function(e) {
    console.log('Configuration window returned: ' + e.response);

    var configuration = JSON.parse(decodeURIComponent(e.response));

    Settings.option('un', configuration.un);
    Settings.option('pw', configuration.pw);
});

/**
 * Salesforce login function
 *
 * Arguments: success (function)   Function to call upon successful login
 *            fail    (function)   Function to call upon failed login
 */
function login(success, fail)
{
    // OAuth login URL
    var url = 'https://login.salesforce.com/services/oauth2/token';

    // Construct the body of the request
    var body = 'grant_type=password' +
                '&client_id=' + AppSettings.clientId +
                '&client_secret=' + AppSettings.clientSecret +
                '&username=' + Settings.option('un') +
                '&password=' + Settings.option('pw');

    // Attempt the login and call the success or failiure function
    ajax({ url: url, method: 'post', type: 'text', data: body,
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
        },
        function(data) {
            // Success, store the token
            var obj = JSON.parse(data);

            token = obj.access_token;

            console.log('Login successful: ' + token);

            success();
        },
        function(error) {
            // Failure!
            var errorText = JSON.stringify(error);

            console.log('login failed: ' + errorText);

            fail(errorText);
        }
    );
}

/**
 * Load the Approvals via the REST Sevice
 */
function loadApprovals()
{
    // Indicate the status on the main screen
    main.body('Loading ...');

    // Create the request
    var req = {
        Username: Settings.option('un'),
    };

    console.log('req: ' + JSON.stringify(req));

    // Call the REST service and handle the success or failure
    ajax({ url: AppSettings.serviceURLprefix + 'GetWorkItems/',
          method: 'post', type: 'json', data: req,
          headers: { 'content-type': 'application/json', 'Authorization' : 'Bearer ' + token },
        },
        function(res) {
            // Success, render the approvals
            console.log('res: ' + JSON.stringify(res));

            main.body('Loaded.');
            renderApprovals(res);
        },
        function(error) {
            // Failure!
            var errorText = JSON.stringify(error);
            console.log('request failed: ' + errorText);

            main.body('Failed:\n' + errorText);
            main.action('select', 'images/reload16x16.png');
        }
    );
}

/**
 * Render the Approvals
 *
 * Arguments:    res    (object)    JSON Response from the GetWorkItems REST Service
 */
function renderApprovals(res)
{
    // If no work items, change the main window text and return
    if (res.WorkItems.length === 0)
    {
        main.body('Nothing to approve.');
        return;
    }

    // Create an array of Menu Items
    var menuItems = [];

    // Create the Menu Items from the Work Items returned by the web service
    for (var i = 0; i < res.WorkItems.length; i++)
    {
        var menuItem = {
            title: res.WorkItems[i].ObjectName,
            subtitle: res.WorkItems[i].WorkItemName,
            id: res.WorkItems[i].WorkItemId,
            fields: res.WorkItems[i].Fields
        };

        menuItems.push(menuItem);
    }

    console.log('menuItems=' + JSON.stringify(menuItems));

    // Add the Menu Items to the menu
    var menu = new UI.Menu({
        sections: [{
            title: 'Approval List',
            items: menuItems
        }]
    });

    // Add the on select event to show the Work Item in more detail
    menu.on('select', function(itemSelectEvent){
        showWorkItem(itemSelectEvent);
    });

    // Show the menu window
    menu.show();
}

/**
 * Show the Work Item in more detail
 *
 * Arguments:    itemSelectEvent    (object)     The menu select event
 */
function showWorkItem(itemSelectEvent)
{
    // Get the menu item from the event
    var menuItem = itemSelectEvent.item;

    var bodyText = '';

    // Build the card body from the fields returned.
    for (var i = 0; i < menuItem.fields.length; i++)
    {
        bodyText += menuItem.fields[i].Name + ': ' + menuItem.fields[i].Value + '\n';
    }

    // There are several windows that will be created as the item
    // is approved, keep track of them to enable them to be removed
    var windowStack = [];

    // Create the Card
    var detailCard = new UI.Card({
        title: menuItem.title,
        subtitle: menuItem.subtitle,
        body: bodyText,
        scrollable: true,
        action: {
            select: 'images/rightarrow12x16.png',
            up: 'images/uparrow16x11.png',
            down: 'images/downarrow16x11.png'
        }
    });

    // Add it to the window stack
    windowStack.push(detailCard);

    // Add the event handler to proceed to the Approval or Rejection
    detailCard.on('click', 'select', function(){
        approveOrReject(itemSelectEvent, windowStack);
    });

    // Show the card
    detailCard.show();
}

/**
 * Approve or Reject the Work Item
 *
 * Arguments:    itemSelectEvent    (object)     The menu select event
 *               windowStack        (array)      The Array of windows relating to this Work Item
 */
function approveOrReject(itemSelectEvent, windowStack)
{
    // Create the Approve or Regject Card
    var approveOrRejectCard = new UI.Card({
        title: 'Approve?',
        action: {
            up: 'images/approve16x16.png',
            down: 'images/reject16x16.png'
        }
    });

    // Add it the stack
    windowStack.push(approveOrRejectCard);

    // Add the event handler for the Approval and Rejection
    approveOrRejectCard.on('click', 'up', function(){
        approveItem(itemSelectEvent, true, windowStack);
    });

    approveOrRejectCard.on('click', 'down', function(){
        approveItem(itemSelectEvent, false, windowStack);
    });

    // show the card
    approveOrRejectCard.show();
}

/**
 * Perform the Approve or Reject
 *
 * Arguments:    itemSelectEvent    (object)     The menu select event
 *               approve            (boolean)    True to approve, false to reject
 *               windowStack        (array)      The Array of windows relating to this Work Item
 */
function approveItem(itemSelectEvent, approve, windowStack)
{
    // Get the menu item from the menu event
    var menuItem = itemSelectEvent.item;

    // Render the Approved/Rejected icon window
    var iconWindow = renderApproved(approve, windowStack);

    // Create the request, from the work item id and the approve/reject boolean
    var req = {
        WorkItemId: menuItem.id,
        Approve: approve
    };

    console.log('req: ' + JSON.stringify(req));

    // Call the REST service and handle the success or failure
    ajax({ url: AppSettings.serviceURLprefix + 'ApproveWorkItem/',
          method: 'post', type: 'json', data: req,
          headers: { 'content-type': 'application/json', 'Authorization' : 'Bearer ' + token },
        },
        function(res) {
            // Success
            console.log('res: ' + JSON.stringify(res));

            // Call the function to clear the icon and hide all the windows in the stack
            iconWindow.hideAll();

            // Remove the item form the menu
            itemSelectEvent.section.items.splice(itemSelectEvent.itemIndex, 1);

            // If no items left then hide the menu and change the main window text
            if (itemSelectEvent.section.items.length === 0)
            {
                main.body('Nothing to approve.');
                itemSelectEvent.menu.hide();
                return;
            }

        },
        function(error) {
            // Failure!
            var errorText = JSON.stringify(error);
            console.log('request failed: ' + errorText);

            // Hide the icon window and render the error
            iconWindow.hide();
            renderError(errorText);
        }
    );
}

/**
 * Render the approved / rejected icon
 *
 * Arguments:    approve        (boolean)    True to show the approve icon, false to show the reject icon
 *               windowStack    (array)      The Array of windows relating to the Work Item
 *
 * Returns:      (object)    The created window
 */
function renderApproved(approve, windowStack)
{
    // Create a new Window
    var win = new UI.Window();

    // Add it to the stack
    windowStack.push(win);

    // Assign the image
    var img = approve ? 'images/approve128x128.png' : 'images/reject128x128.png';

    // Create the image
    var image = new UI.Image({
        position: new Vector2(8, 10),
        size: new Vector2(128, 128),
        backgroundColor: 'white',
        image: img,
        compositing: 'invert'
    });

    // Add it to the window
    win.add(image);

    // Show the window
    win.show();

    // Create the hideAll fucntion to remove it
    win.hideAll = function(){
        // Animimate the image down
        var pos = image.position();
        pos.y = 169;

        image.animate('position', pos, 300);

        // Queue up the removal of the window stack when the
        // animation completes
        image.queue(function(next) {
            hideWindowStack(windowStack);
            next();
        });
    };

    // Return the window
    return win;
}

/**
 * Hide the window stack
 *
 * Arguments:    windowStack    (array)      The Array of windows relating to the Work Item
 */
function hideWindowStack(windowStack)
{
    while(windowStack.length > 0)
    {
        var win = windowStack.pop();
        win.hide();
    }
}

/**
 * Render an error
 *
 * Arguments:    err    (string)    The error to render
 */
function renderError(err)
{
    // Create the Card
    var errorCard = new UI.Card({
        title: 'Failed',
        body: err,
        scrollable: true
    });

    // Show the card
    errorCard.show();
}

// Show the Main Card
main.show();

// Login passing in success and failure functions
login(function(res) {
        // Show connected status
        main.body('Connected.');

        // Change the behaviour of the select button to load the approvals
        main.on('click', 'select', function() {
            loadApprovals();
        });

        loadApprovals();
    },
    function(errorText) {
        main.body('Failed:\n' + errorText);
    }
);

You’ll see near the top we have a couple of calls to add event listeners for the Configuration screen. These are used to marshall the username and password to the app storage.

The login to Salesforce is done using a grant_type of password and therefore bypasses the OAuth flow. This isn’t ideal but we can’t supply a call-back URL to the OAuth flow and even if we could we’d have to make the user login via the config screen whenever the session expires.

Once the login is complete we store the token to use for subsequent API requests for retrieving the Work Items to Approve and Approving/Rejecting the Work Items.

The Pebble windowing system is very simple and takes a stack approach. When a window is loaded it pushes the window onto a stack. Clicking the back button removes the current window from the stack and the previous window is shown as does hiding it via the application code.

The flow of the app looks like this:

Flow

The Watch App source is available from my GitHub repo: https://github.com/tscottdev/PebbleSFDCApprovals

I’ll make the Server code available soon or you can cut and paste the code from Part 1.

I’m considering moving the Client Id and Client Secret to the Configuration Screen at some point so that I can publish the App and make it freely available on the Pebble App store. Watch this space, I hope you’ve found this useful.

Useful Links:

CloudPebble IDE: https://cloudpebble.net/ide/

Pebble Developer Portal: https://developer.getpebble.com/

Pebble Store: https://getpebble.com/

 

Advertisements

Author, brainstormer, coder, dad, explorer, four chord trickster, gig goer, home worker, inquisitor, joker, knowledge seeker, likes: marmite, note scribbler, opinionator, poet, quite likes converse, roller skater, six music listener, tea drinker, urban dweller, vinyl spinner, word wrangler, x-factor hater, Yorkshireman (honorary), zombie slayer (lie).

Tagged with: , , , , ,
Posted in apex, code, force.com, pebble watch, salesforce
About Me
Product Services Developer at:
FinancialForce.com
All views expressed here are my own. More about me and contact details here.

Enter your email address to follow this blog and receive notifications of new posts by email.

Copyright (there isn’t any, feel free to reuse!)

CC0
To the extent possible under law, Tony Scott has waived all copyright and related or neighboring rights to MeltedWires.com Examples and Code Samples. This work is published from: United Kingdom.

%d bloggers like this: