Integrating external apps with salesforce using Canvas

Get Started. It's Free
or sign up with your email address
Integrating external apps with salesforce using Canvas by Mind Map: Integrating external apps with salesforce using Canvas

1. Any web app (under HTTPS) can be embedded using Canvas

1.1. Uses JS SDK to talk with SFDC

1.2. Managed as Connected App

1.3. Unlike iFrame, Canvas works well with CORS

1.4. Can be a Chatter Tab of VisualForce page/element (can be a draggable part of a page)

1.4.1. User isn't aware that it's an external app

1.4.1.1. as long as its a Signed Request & not OAuth

1.4.2. In the future you'll be able to embed them everywhere (e.g., inside chatter feed)

1.5. JS SDK provides everything required for the plumbing & common functions

1.6. Integration can also be using SAML

1.6.1. If you're a SAML provider, you can use salesforce Identity

2. Signed Request POST

2.1. Sent when user clicks the canvas app

2.2. App needs to verify it arrived from SFDC

2.2.1. using the consumer secret

2.3. JSON contents contains

2.3.1. Context

2.3.1.1. application

2.3.1.2. user

2.3.1.3. environment

2.3.1.4. organization

2.3.1.5. links

2.3.1.5.1. for making API REST calls

2.3.2. ...

2.4. CanvasRequest object

3. JS SDK

3.1. $$

3.1.1. canvas SDK alias

3.2. $$.client.ajax

3.2.1. to make AJAX calls to the REST API

4. Examples

4.1. Query list of accounts

4.1.1. salesforceREST.accountLookup = function(sr, callback) { //TODO #1 Provide the URL to perform a REST query var url = sr.context.links.queryUrl + "?q=SELECT+id,+name+FROM Account"; $$.client.ajax(url, {client : sr.client, //TODO #2 Provide the HTTP method needed for a REST query method: "GET", contentType: "application/json", success : function(data) { if ($$.isFunction(callback)) { //TODO #3 Assign returnedAccounts the value of the JSON array containing the // records returned. var returnedAccounts = data.payload.records; var optionStr = ""; for (var acctPos = 0; acctPos < returnedAccounts.length; acctPos = acctPos + 1){ optionStr = optionStr + '<option value="' //TODO #4 Replace the "" with a reference to the Id of the account in the current position. + returnedAccounts[acctPos].Id + '">' //TODO #5 Replace the "" with a reference to the Name of the account in the current position. + returnedAccounts[acctPos].Name + '</option>'; } callback(optionStr); } }, error: function() { alert("I'm sorry we can't populate the menu at this time. Please contact your system administrator if the problem persists."); } }); };

4.2. Add contact

4.2.1. /** * POST a request to salesforce using the REST API. Return the result. */ salesforceREST.contactAdd = function(sr, firstNameInput, lastNameInput, accountIdInput, callback) { //TODO #6 Provide the URL to perform a REST insert. var url = sr.context.links.sobjectUrl + "Contact/"; //TODO #7 Assign the body a list of JSON key-value pairs that include the firstName, lastName, // and accountId fields. var body = { firstName: firstNameInput, lastName: lastNameInput, accountId: accountIdInput }; $$.client.ajax(url, {client : sr.client, //TODO #8 Provide the HTTP method needed for a REST query method: "POST", contentType: "application/json", data: JSON.stringify(body), success : function(data) { if ($$.isFunction(callback)) { callback(data); } }, error: function() { alert("I'm sorry we can't handle your request at this time. Please contact your system administrator if the problem persists."); } }); };

4.3. Full example

4.3.1. Template & JS

4.3.1.1. <%-- Copyright (c) 2011, salesforce.com, inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --%> <%@ page import="canvas.SignedRequest" %> <%@ page import="java.util.Map" %> <% // Pull the signed request out of the request body and verify/decode it. Map<String, String[]> parameters = request.getParameterMap(); String[] signedRequest = parameters.get("signed_request"); if (signedRequest == null) {%> This App must be invoked via a signed request!<% return; } //In a production level application, you would not store your consumer secret directly //in the code. You might have something like: String yourConsumerSecret=System.getenv("CANVAS_CONSUMER_SECRET"); //TODO #1 Add your consumer secret. String yourConsumerSecret="8774399243183795538"; String signedRequestJson = SignedRequest.verifyAndDecodeAsJson(signedRequest[0], yourConsumerSecret); System.out.println("SignedRequest is: \n" + signedRequestJson); %> <!DOCTYPE html> <html> <head> <title>Force.com Canvas Workshop - Dreamforce '13</title> <link rel="stylesheet" type="text/css" href="/sdk/css/canvas.css" /> <script type="text/javascript" src="/sdk/js/canvas-all.js"></script> <script type="text/javascript" src="/scripts/json2.js"></script> <!-- For testing purposes, we're adding a parameter here to force the browser to refresh the javascript file, as it changes during the exercises. --> <script type="text/javascript" src="/scripts/salesforceREST.js?<%= java.lang.Math.random() %>"></script> <script> if (self === top) { // Not in Iframe alert("This canvas app must be included within an iframe"); }; Sfdc.canvas(function() { var sr = JSON.parse('<%=signedRequestJson%>'); var photoUri = sr.context.user.profileThumbnailUrl + "?oauth_token=" + sr.client.oauthToken; /** * Check if we are in sites/communities. If so, derive the url accordingly. */ var isSites=null != sr.context.user.networkId; var siteHost = isSites ? sr.context.user.siteUrl : sr.client.instanceUrl; if (siteHost.lastIndexOf("/") == siteHost.length-1){ siteHost = siteHost.substring(0,siteHost.length-1); }; /** * Using the context provided by the request from Salesforce, populate the top elements of the form. */ Sfdc.canvas.byId("fullname").innerHTML = sr.context.user.fullName; Sfdc.canvas.byId("firstname").innerHTML = sr.context.user.firstName; Sfdc.canvas.byId("lastname").innerHTML = sr.context.user.lastName; Sfdc.canvas.byId("profile").src = (photoUri.indexOf("http")==0 ? "" :siteHost) + photoUri; //TODO #2: Define the username, email, and company variables using Javascript. // (You will replace the double quotation marks.) Sfdc.canvas.byId("username").innerHTML = sr.context.user.userName; Sfdc.canvas.byId("email").innerHTML = sr.context.user.email; Sfdc.canvas.byId("company").innerHTML = sr.context.organization.name; /** * Using the information returned in the context, we will request the current Salesforce * account list for display in the data entry form. */ salesforceREST.accountLookup(sr, function (data) { Sfdc.canvas.byId("sfdc-account-list").innerHTML = Sfdc.canvas.byId("sfdc-account-list").innerHTML + data; }); /** * If the submission includes an account, then we will POST a request to salesforce using the REST API * to add the contact. Otherwise, we will request that the user select an account. */ contactTransfer = function() { var fname = Sfdc.canvas.byId("firstName-input-field").value; var lname = Sfdc.canvas.byId("lastName-input-field").value; var acctId = Sfdc.canvas.byId("sfdc-account-list").value; if (acctId =="NULL") { alert("Please select an account before submitting your contact."); } else { salesforceREST.contactAdd(sr, fname, lname, acctId, function(data) { if (data.statusText =="Bad Request") { var badData = data.payload; var errorMsg = ""; for (var errorPos = 0; errorPos < badData.length; errorPos = errorPos + 1){ errorMsg = errorMsg + badData[errorPos].message + "\n"; } Sfdc.canvas.byId("status").innerHTML = errorMsg; } else { Sfdc.canvas.byId("status").innerHTML = data.statusText; Sfdc.canvas.byId("firstName-input-field").value = ""; Sfdc.canvas.byId("lastName-input-field").value = ""; Sfdc.canvas.byId("sfdc-account-list")[0].selected = true; } }); } }; }); </script> </head> <body> <div id="page"> <div id="content"> <div id="header"> <h1 >Hello <span id="fullname"></span>!</h1> <h2>Welcome to Galaxy World Movers Contact Application</h2> </div> <div id="canvas-content"> <h1>Canvas Request</h1> <h2>Below is some information received in the Canvas Request:</h2> <div id="canvas-request"> <table border="0" width="100%"> <tr> <td></td> <td><b>First Name: </b><span id="firstname"></span></td> <td><b>Last Name: </b><span id="lastname"></span></td> </tr> <tr> <td><img id="profile" border="0" src="" /></td> <td><b>Username: </b><span id="username"></span></td> <td colspan="2"><b>Email Address: </b><span id="email"></span></td> </tr> <tr> <td></td> <td colspan="3"><b>Company: </b><span id="company"></span></td> </tr> </table> </div> <div id="canvas-contactEntry"> <h1>Galaxy World Movers Contact Information</h1> <table border="0" width="100%"> <tr> <td width="15%"><b>Salesforce Account:&nbsp</b></td> <td colspan="4"> <select name="sfdc-account-list" id="sfdc-account-list" > <option value="NULL">Please select an account:</option> </select> </td> </tr> <tr> <td width="15%"><b>New Contact:&nbsp</b></td> <td width="35%">First Name: <input id="firstName-input-field" type="text" x-webkit-speech/></td> <td width="35%">Last Name: <input id="lastName-input-field" type="text" x-webkit-speech/></td> <td width="8%"><button onclick="contactTransfer()" id="contact-submit" type="submit" >Submit</button></td> <td width="8%"><strong><span id="status" style="color:green"></strong></span></td> </tr> </table> </div> </div> </div> <div id="footercont"> <div id="footerleft"> <p>Powered By: <a title="Heroku" href="#" onclick="window.top.location.href='http://www.heroku.com'"><strong>Heroku</strong></a></p> </div> <div id="footerright"> <p>Salesforce: <a title="Safe Harbor" href="http://www.salesforce.com/company/investor/safe_harbor.jsp"><strong>SafeHarbor</strong></a></p> </div> </div> </div> </body> </html>

4.3.2. JS

4.3.2.1. // Copyright (c) 2011, salesforce.com, inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are permitted provided // that the following conditions are met: // // Redistributions of source code must retain the above copyright notice, this list of conditions and the // following disclaimer. // // Redistributions in binary form must reproduce the above copyright notice, this list of conditions and // the following disclaimer in the documentation and/or other materials provided with the distribution. // // Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or // promote products derived from this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) // HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. var salesforceREST; if (!salesforceREST) { salesforceREST = {}; } (function ($$) { "use strict"; salesforceREST.accountLookup = function(sr, callback) { //TODO #1 Provide the URL to perform a REST query var url = sr.context.links.queryUrl + "?q=SELECT+id,+name+FROM Account"; $$.client.ajax(url, {client : sr.client, //TODO #2 Provide the HTTP method needed for a REST query method: "GET", contentType: "application/json", success : function(data) { if ($$.isFunction(callback)) { //TODO #3 Assign returnedAccounts the value of the JSON array containing the // records returned. var returnedAccounts = data.payload.records; var optionStr = ""; for (var acctPos = 0; acctPos < returnedAccounts.length; acctPos = acctPos + 1){ optionStr = optionStr + '<option value="' //TODO #4 Replace the "" with a reference to the Id of the account in the current position. + returnedAccounts[acctPos].Id + '">' //TODO #5 Replace the "" with a reference to the Name of the account in the current position. + returnedAccounts[acctPos].Name + '</option>'; } callback(optionStr); } }, error: function() { alert("I'm sorry we can't populate the menu at this time. Please contact your system administrator if the problem persists."); } }); }; /** * POST a request to salesforce using the REST API. Return the result. */ salesforceREST.contactAdd = function(sr, firstNameInput, lastNameInput, accountIdInput, callback) { //TODO #6 Provide the URL to perform a REST insert. var url = sr.context.links.sobjectUrl + "Contact/"; //TODO #7 Assign the body a list of JSON key-value pairs that include the firstName, lastName, // and accountId fields. var body = { firstName: firstNameInput, lastName: lastNameInput, accountId: accountIdInput }; $$.client.ajax(url, {client : sr.client, //TODO #8 Provide the HTTP method needed for a REST query method: "POST", contentType: "application/json", data: JSON.stringify(body), success : function(data) { if ($$.isFunction(callback)) { callback(data); } }, error: function() { alert("I'm sorry we can't handle your request at this time. Please contact your system administrator if the problem persists."); } }); }; }(Sfdc.canvas));