├── .gitignore ├── css └── style.css ├── img └── ionic.png ├── js ├── config.js ├── controllers.js ├── app.js └── forceng.js ├── bootconfig.json ├── templates ├── account-list.html ├── account.html ├── contact-list.html ├── menu.html ├── contact.html └── edit-contact.html ├── index.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gitignore -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | /* Empty. Add your own CSS if you like */ 2 | -------------------------------------------------------------------------------- /img/ionic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ionic-team/ionic-starter-salesforce/HEAD/img/ionic.png -------------------------------------------------------------------------------- /js/config.js: -------------------------------------------------------------------------------- 1 | angular.module('config', []) 2 | 3 | .constant('forcengOptions', {}) 4 | 5 | // baseURL should be left to empty string. This value is only used when you want to use the same app in a Visualforce 6 | // page where you have to account for the path to the static resource. In that case the config module is created from 7 | // within index.vf where the path to the static resource can be obtained. 8 | .constant('baseURL', ''); 9 | 10 | -------------------------------------------------------------------------------- /bootconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "remoteAccessConsumerKey": "3MVG9Iu66FKeHhINkB1l7xt7kR8czFcCTUhgoA8Ol2Ltf1eYHOU4SqQRSEitYFDUpqRWcoQ2.dBv_a1Dyu5xa", 3 | "oauthRedirectURI": "testsfdc:///mobilesdk/detect/oauth/done", 4 | "oauthScopes": [ 5 | "web", 6 | "api" 7 | ], 8 | "isLocal": true, 9 | "startPage": "index.html", 10 | "errorPage": "error.html", 11 | "shouldAuthenticate": true, 12 | "attemptOfflineLoad": false 13 | } -------------------------------------------------------------------------------- /templates/account-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{account.Name}} 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /templates/account.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |

{{account.Name}}

6 |
7 | 8 | Call

{{account.Phone}}

9 |
10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /templates/contact-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{contact.Name}} 15 |

{{contact.Title}}

16 |
17 |
18 |
19 | 20 |
21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /templates/menu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

Menu

17 |
18 | 19 | 20 | 21 | Contacts 22 | 23 | 24 | Accounts 25 | 26 | 27 | Logout 28 | 29 | 30 | 31 |
32 | 33 |
34 | -------------------------------------------------------------------------------- /templates/contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Edit 5 | 6 | 7 | 8 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /templates/edit-contact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 13 | 17 | 21 | 25 | 29 | 33 |
34 |
35 | 36 |
37 | -------------------------------------------------------------------------------- /js/controllers.js: -------------------------------------------------------------------------------- 1 | angular.module('starter.controllers', ['forceng']) 2 | 3 | .controller('AppCtrl', function ($scope, force) { 4 | 5 | $scope.logout = function() { 6 | force.logout(); 7 | }; 8 | 9 | }) 10 | 11 | .controller('ContactListCtrl', function ($scope, force) { 12 | 13 | force.query('select id, name, title from contact limit 50').then( 14 | function (data) { 15 | $scope.contacts = data.records; 16 | }, 17 | function (error) { 18 | alert("Error Retrieving Contacts"); 19 | console.log(error); 20 | }); 21 | 22 | }) 23 | 24 | .controller('ContactCtrl', function ($scope, $stateParams, force) { 25 | 26 | force.retrieve('contact', $stateParams.contactId, 'id,name,title,phone,mobilephone,email').then( 27 | function (contact) { 28 | $scope.contact = contact; 29 | }); 30 | 31 | 32 | }) 33 | 34 | .controller('EditContactCtrl', function ($scope, $stateParams, $ionicNavBarDelegate, force) { 35 | 36 | force.retrieve('contact', $stateParams.contactId, 'id,firstname,lastname,title,phone,mobilephone,email').then( 37 | function (contact) { 38 | $scope.contact = contact; 39 | }); 40 | 41 | $scope.save = function () { 42 | force.update('contact', $scope.contact).then( 43 | function (response) { 44 | $ionicNavBarDelegate.back(); 45 | }, 46 | function() { 47 | alert("An error has occurred."); 48 | }); 49 | }; 50 | 51 | }) 52 | 53 | .controller('CreateContactCtrl', function ($scope, $stateParams, $ionicNavBarDelegate, force) { 54 | 55 | $scope.contact = {}; 56 | 57 | $scope.save = function () { 58 | force.create('contact', $scope.contact).then( 59 | function (response) { 60 | $ionicNavBarDelegate.back(); 61 | }, 62 | function() { 63 | alert("An error has occurred."); 64 | }); 65 | }; 66 | 67 | }) 68 | 69 | .controller('AccountListCtrl', function ($scope, force) { 70 | 71 | force.query('select id, name from account limit 50').then( 72 | function (data) { 73 | $scope.accounts = data.records; 74 | }); 75 | 76 | }) 77 | 78 | .controller('AccountCtrl', function ($scope, $stateParams, force) { 79 | 80 | force.retrieve('account', $stateParams.accountId, 'id,name,phone,billingaddress').then( 81 | function (account) { 82 | $scope.account = account; 83 | }); 84 | 85 | }); 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Starter pack for building Ionic apps on top of the Salesforce platform created as a partnership between Ionic and Salesforce. 2 | 3 | This is an addon starter template for the [Ionic Framework](http://ionicframework.com/). The template installs a starter application that uses Salesforce OAuth to authenticate, and the Salesforce REST APIs to access and manipulate Salesforce data. 4 | 5 | ## How to use this template 6 | 7 | To use this starter pack, you'll need to have [installed Ionic](http://ionicframework.com/getting-started) and Cordova: 8 | 9 | ```bash 10 | $ sudo npm install -g ionic cordova 11 | ``` 12 | 13 | Then, all you need to do is run: 14 | ``` 15 | $ ionic start myApp salesforce 16 | $ cd myApp 17 | $ ionic platforms update ios 18 | $ ionic plugin add https://github.com/forcedotcom/SalesforceMobileSDK-CordovaPlugin 19 | ``` 20 | 21 | ## Running on device 22 | 23 | Substitute ios for android in the instructioons below, but if you can, the ios development toolchain is a lot easier to work with until you need to do anything custom to Android. 24 | 25 | 1. Build the project: 26 | 27 | ```bash 28 | $ ionic build ios 29 | ``` 30 | 31 | 1. Run the app in the emulator 32 | 33 | ```bash 34 | $ ionic emulate ios 35 | ``` 36 | 37 | 1. ... or (for iOS) open **platforms/ios/myApp.xcodeproj** in Xcode and run the app on your device. If the build fails in Xcode, select the myApp target, click the **Build Settings** tab, search for **bitcode**, select **No** for **Enable Bitcode**, and try again. 38 | 39 | 40 | ## Running in the browser 41 | 42 | Because of the browser's cross-origin restrictions, your Ionic application hosted on your own server (or localhost) will not be able to make API calls directly to the *.salesforce.com domain. The solution is to proxy your API calls through your own server. You can use your own proxy server or use [ForceServer](https://github.com/ccoenraets/force-server), a simple development server for Force.com. It provides two main features: 43 | 44 | - A **Proxy Server** to avoid cross-domain policy issues when invoking Salesforce REST services. (The Chatter API supports CORS, but other APIs don’t yet) 45 | - A **Local Web Server** to (1) serve the OAuth callback URL defined in your Connected App, and (2) serve the whole app during development and avoid cross-domain policy issues when loading files (for example, templates) from the local file system. 46 | 47 | To run the application in the browser using ForceServer: 48 | 49 | 1. Install ForceServer 50 | 51 | ```bash 52 | $ sudo npm install -g force-server 53 | ``` 54 | 55 | 2. Navigate (cd) to your Ionic app's **www** directory 56 | 57 | 3. Start the server 58 | 59 | ``` 60 | force-server 61 | ``` 62 | 63 | This command will start the server on port 8200, and automatically load your app (http://localhost:8200) in a browser window. You'll see the Salesforce login window (make sure you enable the popup), and the list of contacts will appear after you log in. If you don’t have a free Salesforce Developer Edition to log in to, you can create one [here](http://developer.salesforce.com/signup). 64 | 65 | You can change the port number and the web root. Type the following command for more info: 66 | 67 | ``` 68 | force-server --help 69 | ``` 70 | 71 | ## OAuth and REST 72 | 73 | This template uses [forceng](https://github.com/ccoenraets/forceng), a micro-library that makes it easy to use the Salesforce REST APIs in AngularJS applications. forceng allows you to easily login into Salesforce using OAuth, and to access your Salesforce data using a simple API. 74 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | // Ionic Starter App 2 | 3 | // angular.module is a global place for creating, registering and retrieving Angular modules 4 | // 'starter' is the name of this angular module example (also set in a attribute in index.html) 5 | // the 2nd parameter is an array of 'requires' 6 | // 'starter.controllers' is found in controllers.js 7 | angular.module('starter', ['ionic', 'forceng', 'starter.controllers', 'config']) 8 | 9 | .run(function ($ionicPlatform, $state, force, forcengOptions) { 10 | 11 | $ionicPlatform.ready(function () { 12 | 13 | // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard 14 | // for form inputs) 15 | if (window.cordova && window.cordova.plugins && window.cordova.plugins.Keyboard) { 16 | window.cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); 17 | window.cordova.plugins.Keyboard.disableScroll(true); 18 | } 19 | if (window.StatusBar) { 20 | // org.apache.cordova.statusbar required 21 | StatusBar.styleDefault(); 22 | } 23 | 24 | // Initialize forceng 25 | force.init(forcengOptions); 26 | 27 | if (forcengOptions.accessToken) { 28 | // If the accessToken was provided (typically when running the app from within a Visualforce page, 29 | // go straight to the contact list 30 | $state.go('app.contactlist'); 31 | } else { 32 | // Otherwise (the app is probably running as a standalone web app or as a hybrid local app with the 33 | // Mobile SDK, login first.) 34 | force.login().then( 35 | function () { 36 | $state.go('app.contactlist'); 37 | }, 38 | function(error) { 39 | alert("Login was not successful"); 40 | }); 41 | } 42 | 43 | }); 44 | }) 45 | 46 | .config(function ($stateProvider, $urlRouterProvider, baseURL) { 47 | 48 | // baseURL (defined in the config.js module) is only there to support running the same app as a Mobile SDK 49 | // hybrid local and hybrid remote app (where the app is run from withing a Visualforce page). When running the 50 | // app inside a Visualforce page, you have to account for the path of the app's static resource. To accomplish 51 | // that, you create the config module from within the VF page (as opposed to importing config.js), and set 52 | // baseURL to the app's static resource path. 53 | 54 | $stateProvider 55 | 56 | .state('app', { 57 | url: "/app", 58 | abstract: true, 59 | templateUrl: baseURL + "templates/menu.html", 60 | controller: 'AppCtrl' 61 | }) 62 | 63 | .state('app.contactlist', { 64 | url: "/contactlist", 65 | views: { 66 | 'menuContent': { 67 | templateUrl: baseURL + "templates/contact-list.html", 68 | controller: 'ContactListCtrl' 69 | } 70 | } 71 | }) 72 | 73 | .state('app.contact', { 74 | url: "/contacts/:contactId", 75 | views: { 76 | 'menuContent': { 77 | templateUrl: baseURL + "templates/contact.html", 78 | controller: 'ContactCtrl' 79 | } 80 | } 81 | }) 82 | 83 | .state('app.edit-contact', { 84 | url: "/edit-contact/:contactId", 85 | views: { 86 | 'menuContent': { 87 | templateUrl: baseURL + "templates/edit-contact.html", 88 | controller: 'EditContactCtrl' 89 | } 90 | } 91 | }) 92 | 93 | .state('app.add-contact', { 94 | url: "/create-contact", 95 | views: { 96 | 'menuContent': { 97 | templateUrl: baseURL + "templates/edit-contact.html", 98 | controller: 'CreateContactCtrl' 99 | } 100 | } 101 | }) 102 | 103 | .state('app.accountlist', { 104 | url: "/accountlist", 105 | views: { 106 | 'menuContent': { 107 | templateUrl: baseURL + "templates/account-list.html", 108 | controller: 'AccountListCtrl' 109 | } 110 | } 111 | }) 112 | 113 | .state('app.account', { 114 | url: "/accounts/:accountId", 115 | views: { 116 | 'menuContent': { 117 | templateUrl: baseURL + "templates/account.html", 118 | controller: 'AccountCtrl' 119 | } 120 | } 121 | }); 122 | 123 | }); 124 | -------------------------------------------------------------------------------- /js/forceng.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ForceNG - REST toolkit for Salesforce.com 3 | * Author: Christophe Coenraets @ccoenraets 4 | * Version: 0.6.1 5 | */ 6 | angular.module('forceng', []) 7 | 8 | .factory('force', function ($rootScope, $q, $window, $http) { 9 | 10 | // The login URL for the OAuth process 11 | // To override default, pass loginURL in init(props) 12 | var loginURL = 'https://login.salesforce.com', 13 | 14 | // The Connected App client Id. Default app id provided - Not for production use. 15 | // This application supports http://localhost:8200/oauthcallback.html as a valid callback URL 16 | // To override default, pass appId in init(props) 17 | appId = '3MVG9fMtCkV6eLheIEZplMqWfnGlf3Y.BcWdOf1qytXo9zxgbsrUbS.ExHTgUPJeb3jZeT8NYhc.hMyznKU92', 18 | 19 | // The force.com API version to use. 20 | // To override default, pass apiVersion in init(props) 21 | apiVersion = 'v33.0', 22 | 23 | // Keep track of OAuth data (access_token, refresh_token, and instance_url) 24 | oauth, 25 | 26 | // By default we store fbtoken in sessionStorage. This can be overridden in init() 27 | tokenStore = {}, 28 | 29 | // if page URL is http://localhost:3000/myapp/index.html, context is /myapp 30 | context = window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/")), 31 | 32 | // if page URL is http://localhost:3000/myapp/index.html, serverURL is http://localhost:3000 33 | serverURL = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''), 34 | 35 | // if page URL is http://localhost:3000/myapp/index.html, baseURL is http://localhost:3000/myapp 36 | baseURL = serverURL + context, 37 | 38 | // Only required when using REST APIs in an app hosted on your own server to avoid cross domain policy issues 39 | // To override default, pass proxyURL in init(props) 40 | proxyURL = baseURL, 41 | 42 | // if page URL is http://localhost:3000/myapp/index.html, oauthCallbackURL is http://localhost:3000/myapp/oauthcallback.html 43 | // To override default, pass oauthCallbackURL in init(props) 44 | oauthCallbackURL = baseURL + '/oauthcallback.html', 45 | 46 | // Because the OAuth login spans multiple processes, we need to keep the login success and error handlers as a variables 47 | // inside the module instead of keeping them local within the login function. 48 | deferredLogin, 49 | 50 | // Reference to the Salesforce OAuth plugin 51 | oauthPlugin, 52 | 53 | // Whether or not to use a CORS proxy. Defaults to false if app running in Cordova or in a VF page 54 | // Can be overriden in init() 55 | useProxy = (window.cordova || window.SfdcApp) ? false : true; 56 | 57 | /* 58 | * Determines the request base URL. 59 | */ 60 | function getRequestBaseURL() { 61 | 62 | var url; 63 | 64 | if (useProxy) { 65 | url = proxyURL; 66 | } else if (oauth.instance_url) { 67 | url = oauth.instance_url; 68 | } else { 69 | url = serverURL; 70 | } 71 | 72 | // dev friendly API: Remove trailing '/' if any so url + path concat always works 73 | if (url.slice(-1) === '/') { 74 | url = url.slice(0, -1); 75 | } 76 | 77 | return url; 78 | } 79 | 80 | function parseQueryString(queryString) { 81 | var qs = decodeURIComponent(queryString), 82 | obj = {}, 83 | params = qs.split('&'); 84 | params.forEach(function (param) { 85 | var splitter = param.split('='); 86 | obj[splitter[0]] = splitter[1]; 87 | }); 88 | return obj; 89 | } 90 | 91 | function toQueryString(obj) { 92 | var parts = [], 93 | i; 94 | for (i in obj) { 95 | if (obj.hasOwnProperty(i)) { 96 | parts.push(encodeURIComponent(i) + "=" + encodeURIComponent(obj[i])); 97 | } 98 | } 99 | return parts.join("&"); 100 | } 101 | 102 | function refreshTokenWithPlugin(deferred) { 103 | oauthPlugin.authenticate( 104 | function (response) { 105 | oauth.access_token = response.accessToken; 106 | tokenStore.forceOAuth = JSON.stringify(oauth); 107 | deferred.resolve(); 108 | }, 109 | function () { 110 | console.log('Error refreshing oauth access token using the oauth plugin'); 111 | deferred.reject(); 112 | }); 113 | } 114 | 115 | function refreshTokenWithHTTPRequest(deferred) { 116 | var params = { 117 | 'grant_type': 'refresh_token', 118 | 'refresh_token': oauth.refresh_token, 119 | 'client_id': appId 120 | }, 121 | 122 | headers = {}, 123 | 124 | url = useProxy ? proxyURL : loginURL; 125 | 126 | // dev friendly API: Remove trailing '/' if any so url + path concat always works 127 | if (url.slice(-1) === '/') { 128 | url = url.slice(0, -1); 129 | } 130 | 131 | url = url + '/services/oauth2/token?' + toQueryString(params); 132 | 133 | if (!useProxy) { 134 | headers["Target-URL"] = loginURL; 135 | } 136 | 137 | $http({ 138 | headers: headers, 139 | method: 'POST', 140 | url: url, 141 | params: params 142 | }) 143 | .success(function (data, status, headers, config) { 144 | console.log('Token refreshed'); 145 | oauth.access_token = data.access_token; 146 | tokenStore.forceOAuth = JSON.stringify(oauth); 147 | deferred.resolve(); 148 | }) 149 | .error(function (data, status, headers, config) { 150 | console.log('Error while trying to refresh token'); 151 | deferred.reject(); 152 | }); 153 | } 154 | 155 | function refreshToken() { 156 | var deferred = $q.defer(); 157 | if (oauthPlugin) { 158 | refreshTokenWithPlugin(deferred); 159 | } else { 160 | refreshTokenWithHTTPRequest(deferred); 161 | } 162 | return deferred.promise; 163 | } 164 | 165 | /** 166 | * Initialize ForceNG 167 | * @param params 168 | * appId (optional) 169 | * loginURL (optional) 170 | * proxyURL (optional) 171 | * oauthCallbackURL (optional) 172 | * apiVersion (optional) 173 | * accessToken (optional) 174 | * instanceURL (optional) 175 | * refreshToken (optional) 176 | */ 177 | function init(params) { 178 | 179 | if (params) { 180 | appId = params.appId || appId; 181 | apiVersion = params.apiVersion || apiVersion; 182 | loginURL = params.loginURL || loginURL; 183 | oauthCallbackURL = params.oauthCallbackURL || oauthCallbackURL; 184 | proxyURL = params.proxyURL || proxyURL; 185 | useProxy = params.useProxy === undefined ? useProxy : params.useProxy; 186 | 187 | if (params.accessToken) { 188 | if (!oauth) oauth = {}; 189 | oauth.access_token = params.accessToken; 190 | } 191 | 192 | if (params.instanceURL) { 193 | if (!oauth) oauth = {}; 194 | oauth.instance_url = params.instanceURL; 195 | } 196 | 197 | if (params.refreshToken) { 198 | if (!oauth) oauth = {}; 199 | oauth.refresh_token = params.refreshToken; 200 | } 201 | } 202 | 203 | console.log("useProxy: " + useProxy); 204 | } 205 | 206 | /** 207 | * Discard the OAuth access_token. Use this function to test the refresh token workflow. 208 | */ 209 | function discardToken() { 210 | delete oauth.access_token; 211 | tokenStore.forceOAuth = JSON.stringify(oauth); 212 | } 213 | 214 | /** 215 | * Called internally either by oauthcallback.html (when the app is running the browser) 216 | * @param url - The oauthCallbackURL called by Salesforce at the end of the OAuth workflow. Includes the access_token in the querystring 217 | */ 218 | function oauthCallback(url) { 219 | 220 | // Parse the OAuth data received from Facebook 221 | var queryString, 222 | obj; 223 | 224 | if (url.indexOf("access_token=") > 0) { 225 | queryString = url.substr(url.indexOf('#') + 1); 226 | obj = parseQueryString(queryString); 227 | oauth = obj; 228 | tokenStore['forceOAuth'] = JSON.stringify(oauth); 229 | if (deferredLogin) deferredLogin.resolve(); 230 | } else if (url.indexOf("error=") > 0) { 231 | queryString = decodeURIComponent(url.substring(url.indexOf('?') + 1)); 232 | obj = parseQueryString(queryString); 233 | if (deferredLogin) deferredLogin.reject(obj); 234 | } else { 235 | if (deferredLogin) deferredLogin.reject({status: 'access_denied'}); 236 | } 237 | } 238 | 239 | /** 240 | * Login to Salesforce using OAuth. If running in a Browser, the OAuth workflow happens in a a popup window. 241 | */ 242 | function login() { 243 | deferredLogin = $q.defer(); 244 | if (window.cordova) { 245 | loginWithPlugin(); 246 | } else { 247 | loginWithBrowser(); 248 | } 249 | return deferredLogin.promise; 250 | } 251 | 252 | function loginWithPlugin() { 253 | document.addEventListener("deviceready", function () { 254 | oauthPlugin = cordova.require("com.salesforce.plugin.oauth"); 255 | if (!oauthPlugin) { 256 | console.error('Salesforce Mobile SDK OAuth plugin not available'); 257 | if (deferredLogin) deferredLogin.reject({status: 'Salesforce Mobile SDK OAuth plugin not available'}); 258 | return; 259 | } 260 | oauthPlugin.getAuthCredentials( 261 | function (creds) { 262 | // Initialize ForceJS 263 | init({accessToken: creds.accessToken, instanceURL: creds.instanceUrl, refreshToken: creds.refreshToken}); 264 | if (deferredLogin) deferredLogin.resolve(); 265 | }, 266 | function (error) { 267 | console.log(error); 268 | if (deferredLogin) deferredLogin.reject(error); 269 | } 270 | ); 271 | }, false); 272 | } 273 | 274 | function loginWithBrowser() { 275 | console.log('loginURL: ' + loginURL); 276 | console.log('oauthCallbackURL: ' + oauthCallbackURL); 277 | 278 | var loginWindowURL = loginURL + '/services/oauth2/authorize?client_id=' + appId + '&redirect_uri=' + 279 | oauthCallbackURL + '&response_type=token'; 280 | window.open(loginWindowURL, '_blank', 'location=no'); 281 | } 282 | 283 | /** 284 | * Gets the user's ID (if logged in) 285 | * @returns {string} | undefined 286 | */ 287 | function getUserId() { 288 | return (typeof(oauth) !== 'undefined') ? oauth.id.split('/').pop() : undefined; 289 | } 290 | 291 | /** 292 | * Check the login status 293 | * @returns {boolean} 294 | */ 295 | function isAuthenticated() { 296 | return (oauth && oauth.access_token) ? true : false; 297 | } 298 | 299 | /** 300 | * Lets you make any Salesforce REST API request. 301 | * @param obj - Request configuration object. Can include: 302 | * method: HTTP method: GET, POST, etc. Optional - Default is 'GET' 303 | * path: path in to the Salesforce endpoint - Required 304 | * params: queryString parameters as a map - Optional 305 | * data: JSON object to send in the request body - Optional 306 | */ 307 | function request(obj) { 308 | 309 | var method = obj.method || 'GET', 310 | headers = {}, 311 | url = getRequestBaseURL(), 312 | deferred = $q.defer(); 313 | 314 | if (!oauth || (!oauth.access_token && !oauth.refresh_token)) { 315 | deferred.reject('No access token. Login and try again.'); 316 | return deferred.promise; 317 | } 318 | 319 | // dev friendly API: Add leading '/' if missing so url + path concat always works 320 | if (obj.path.charAt(0) !== '/') { 321 | obj.path = '/' + obj.path; 322 | } 323 | 324 | url = url + obj.path; 325 | 326 | headers["Authorization"] = "Bearer " + oauth.access_token; 327 | if (obj.contentType) { 328 | headers["Content-Type"] = obj.contentType; 329 | } 330 | if (useProxy) { 331 | headers["Target-URL"] = oauth.instance_url; 332 | } 333 | 334 | $http({ 335 | headers: headers, 336 | method: method, 337 | url: url, 338 | params: obj.params, 339 | data: obj.data 340 | }) 341 | .success(function (data, status, headers, config) { 342 | deferred.resolve(data); 343 | }) 344 | .error(function (data, status, headers, config) { 345 | if (status === 401 && oauth.refresh_token) { 346 | refreshToken() 347 | .success(function () { 348 | // Try again with the new token 349 | request(obj); 350 | }) 351 | .error(function () { 352 | console.error(data); 353 | deferred.reject(data); 354 | }); 355 | } else { 356 | console.error(data); 357 | deferred.reject(data); 358 | } 359 | 360 | }); 361 | 362 | return deferred.promise; 363 | } 364 | 365 | /** 366 | * Execute SOQL query 367 | * @param soql 368 | * @returns {*} 369 | */ 370 | function query(soql) { 371 | 372 | return request({ 373 | path: '/services/data/' + apiVersion + '/query', 374 | params: {q: soql} 375 | }); 376 | 377 | } 378 | 379 | /** 380 | * Retrieve a record based on its Id 381 | * @param objectName 382 | * @param id 383 | * @param fields 384 | * @returns {*} 385 | */ 386 | function retrieve(objectName, id, fields) { 387 | 388 | return request({ 389 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/' + id, 390 | params: fields ? {fields: fields} : undefined 391 | }); 392 | 393 | } 394 | 395 | /** 396 | * Create a record 397 | * @param objectName 398 | * @param data 399 | * @returns {*} 400 | */ 401 | function create(objectName, data) { 402 | 403 | return request({ 404 | method: 'POST', 405 | contentType: 'application/json', 406 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/', 407 | data: data 408 | }); 409 | 410 | } 411 | 412 | /** 413 | * Update a record 414 | * @param objectName 415 | * @param data 416 | * @returns {*} 417 | */ 418 | function update(objectName, data) { 419 | 420 | var id = data.Id, 421 | fields = angular.copy(data); 422 | 423 | delete fields.attributes; 424 | delete fields.Id; 425 | 426 | return request({ 427 | method: 'POST', 428 | contentType: 'application/json', 429 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/' + id, 430 | params: {'_HttpMethod': 'PATCH'}, 431 | data: fields 432 | }); 433 | 434 | } 435 | 436 | /** 437 | * Delete a record 438 | * @param objectName 439 | * @param id 440 | * @returns {*} 441 | */ 442 | function del(objectName, id) { 443 | 444 | return request({ 445 | method: 'DELETE', 446 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/' + id 447 | }); 448 | 449 | } 450 | 451 | /** 452 | * Upsert a record 453 | * @param objectName 454 | * @param externalIdField 455 | * @param externalId 456 | * @param data 457 | * @returns {*} 458 | */ 459 | function upsert(objectName, externalIdField, externalId, data) { 460 | 461 | return request({ 462 | method: 'PATCH', 463 | contentType: 'application/json', 464 | path: '/services/data/' + apiVersion + '/sobjects/' + objectName + '/' + externalIdField + '/' + externalId, 465 | data: data 466 | }); 467 | 468 | } 469 | 470 | /** 471 | * Convenience function to invoke APEX REST endpoints 472 | * @param pathOrParams 473 | * @param successHandler 474 | * @param errorHandler 475 | */ 476 | function apexrest(pathOrParams) { 477 | 478 | var params; 479 | 480 | if (pathOrParams.substring) { 481 | params = {path: pathOrParams}; 482 | } else { 483 | params = pathOrParams; 484 | 485 | if (params.path.charAt(0) !== "/") { 486 | params.path = "/" + params.path; 487 | } 488 | 489 | if (params.path.substr(0, 18) !== "/services/apexrest") { 490 | params.path = "/services/apexrest" + params.path; 491 | } 492 | } 493 | 494 | return request(params); 495 | } 496 | 497 | /** 498 | * Convenience function to invoke the Chatter API 499 | * @param params 500 | * @param successHandler 501 | * @param errorHandler 502 | */ 503 | function chatter(params) { 504 | 505 | var base = "/services/data/" + apiVersion + "/chatter"; 506 | 507 | if (!params || !params.path) { 508 | errorHandler("You must specify a path for the request"); 509 | return; 510 | } 511 | 512 | if (params.path.charAt(0) !== "/") { 513 | params.path = "/" + params.path; 514 | } 515 | 516 | params.path = base + params.path; 517 | 518 | return request(params); 519 | 520 | } 521 | 522 | // The public API 523 | return { 524 | init: init, 525 | login: login, 526 | getUserId: getUserId, 527 | isAuthenticated: isAuthenticated, 528 | request: request, 529 | query: query, 530 | create: create, 531 | update: update, 532 | del: del, 533 | upsert: upsert, 534 | retrieve: retrieve, 535 | apexrest: apexrest, 536 | chatter: chatter, 537 | discardToken: discardToken, 538 | oauthCallback: oauthCallback 539 | }; 540 | 541 | }); 542 | 543 | // Global function called back by the OAuth login dialog 544 | function oauthCallback(url) { 545 | var injector = angular.element(document.body).injector(); 546 | injector.invoke(function (force) { 547 | force.oauthCallback(url); 548 | }); 549 | } 550 | --------------------------------------------------------------------------------