Marc Paris
May 31, 2019

When building Salesforce custom applications, it is important to understand the dependency on certain optional components to ensure that features aren’t inadvertently mandated by an AppExchange application. It is bad practice for a customer to need to modify the structure of their org or needlessly turn on features when trying to install or set up your application. Your application may need to be installed into an existing global org and feature enablement may necessitate a complicated change control process. Fortunately, most of these issues can be avoided with a little bit of careful planning. In this blog post, we will be examining a few of these issues.

Share Tables and Org Wide Defaults

Salesforce custom objects have corresponding __share objects associated with them. These sharing objects are used to identify which sharing mechanisms have been enabled for each record and will be used to control the visibility and editability of the records. These tables can be accessed and written to in Apex. This allows very complicated rules to be built to control record visibility and editability. Apex managed sharing is an extremely powerful tool that is available when more configurable sharing options won’t meet customer requirements. An example of using Apex managed sharing is presented below:

Id groupId = [Select Id From Group Where DeveloperName = 'Sharing_Group' Limit 1].Id;
CustomObject__c customObject = new CustomObject__c();
customObject.Name = 'abc';
insert customObject;

CustomObject__Share cObjectShare = new CustomObject__share();
cObjectShare.AccessLevel = 'Edit';
cObjectShare.RowCause = 'Manual';
cObjectShare.UserOrGroupId = groupId;
cObjectShare.ParentId = customObject.Id;
insert cObjectShare;

There is a catch with __share objects. If a Salesforce Admin sets the Org Wide Default (OWD) for the object to Public Read Write, the __share Object will no longer exist. Code that statically references the __share object will prevent the Admin from setting the OWDs to Public Read Write. A customer may decide that in their security model, a Public Read/Write sharing model is more efficient and minimizes maintenance. However, the code above would prevent this and the customer would receive an error message. Limiting the admin in this fashion gives your app a bad first impression and may even prevent a user from installing a trial resulting in lost sales.

Public Read/Write

There is a way to ensure that Apex managed sharing code will work as expected if the sharing model is set to private or Public Read only, while silently “going away” when Public Read Write has been selected. By avoiding static dependencies on __share tables and checking for the presence of the __share table and only performing the logic if present, we get the best of both worlds. The following code performs the same operation but is not strongly typed. It will not block the Admin from setting the OWD to Public Read/Write.

Id groupId = [Select Id From Group Where DeveloperName = 'Sharing_Group' Limit 1].Id;

CustomObject__c customObject = new CustomObject__c();
customObject.Name = 'abc';
insert customObject;

Map<String, SObjectType> sObjectTypes = Schema.getGlobalDescribe();

SObjectType sObjType = sObjectTypes.get('CustomObject__Share');

if (sObjType != null) {
   SObject sobj = sObjType.newSObject();
   sobj.put('AccessLevel', 'Edit');
   sobj.put('RowCause', 'Manual');
   sObj.put('UserOrGroupId', groupId);
   sObj.put('ParentId', customObject.Id);
   insert sObj;
} else {
   // Skip the Apex Sharing because OWD is public Read Write
}

Teams, Territories, and Person Accounts

The same approach can be leveraged with other optional features of the platform. The application might want to leverage Teams (Opportunity, Case, or Account) or territories if they are enabled in the platform. However, in many cases, the AppExchange Package shouldn’t force the user to enable these features prior to installing.

Let’s assume a feature of your application is advanced team provisioning capability or territory assignments. You’d like this code to work well with out-of-the-box opportunity sales teams, but you don’t want the install to fail if the customer doesn’t have that feature enabled because this is a small feature of your overall application. By querying for the existence of these objects, the code can leverage them if they are present. The following code in a managed package will cause the install to fail if the target org does not have opportunity teams enabled.

Account acc = new Account(Name = 'My Account');
insert acc;
Opportunity oppty = new Opportunity(CloseDate = Date.today()+ 1, Amount = 100, Name = 'Oppty Name', StageName = 'Prospecting', AccountId = acc.Id);
insert oppty;
OpportunityTeamMember otm = new OpportunityTeamMember (OpportunityId = oppty.Id,UserId = UserInfo.getUserId(),TeamMemberRole = 'Role');
insert otm;

This app can't be installed

The following code will accomplish the same thing as above; however, since there is no compile-time dependency on the OpportunityTeamMember object, it can still be installed in orgs that haven’t enabled Sales Teams. The code will be bypassed if the object doesn’t exist.

Account acc = new Account(Name = 'My Account');
insert acc;
Opportunity oppty = new Opportunity(CloseDate = Date.today()+ 1, Amount = 100, Name = 'Oppty Name', StageName = 'Prospecting', AccountId = acc.Id);
insert oppty;

SObjectType stype = Schema.getGlobalDescribe().get('OpportunityTeamMember');

if (stype != null) {
   SObject obj = stype.newSObject();
   obj.put('OpportunityId',oppty.Id);
   obj.put('UserId', UserInfo.getUserId());
   obj.put('TeamMemberRole', 'Role');
   insert obj;
} else {
   // Sales teams not enabled
}

The approach of querying for the presence of certain domain objects can be used generally with most optional features. Entire objects graphs are present when features such as Territory management are enabled. Most of the Salesforce object domains are listed here.

Person accounts are an optional feature of the platform that is very useful in B2C environments. A PersonAccount has an Account and a Contact record effectively “joined” as one. PersonAccount objects have the contact fields available on the Account object. Statically referencing PersonAccount fields will result in an AppExchange package that only functions with Person Accounts.

Dynamic referencing allows you to build one package that adapts to its environment. This is particularly critical when building an extension to Health Cloud or Financial Services Cloud which can be configured to work with or without Person Accounts. The following code shows how Salesforce can be queried to find out if Person Accounts are enabled — and if so, whether Health Cloud has been configured to use Person Accounts. The code will refer to an Apex “Person” class that is dynamically populated with either Person Account Fields or Account Fields and a related Contact’s Fields.

boolean isPersonAccountEnabled = Schema.sObjectType.Account.fields.getMap().containsKey('IsPersonAccount');
boolean usePersonAccount = false;

if (isPersonAccountEnabled) {
   HealthCloudGA__UsePersonAccount__c upa = HealthCloudGA__UsePersonAccount__c.getInstance();
   usePersonAccount = (upa != null && upa.HealthCloudGA__Enable__c);
}

Person individual = new Person();

if (usePersonAccount) {
   String soql = 'Select Id, FirstName, LastName, PersonEmail, PersonContactId, PersonMailingStreet, PersonMailingCity, PersonMailingState, PersonMailingPostalCode, PersonBirthDate, PersonHomePhone from Account';
   List<sObject> accounts = Database.query(soql);
   SObject personAccount = accounts[0];

   individual.AccountId = String.valueOf(personAccount.get('Id'));
   individual.ContactId = String.valueOf(personAccount.get('PersonContactId'));
   individual.FirstName = String.valueOf(personAccount.get('FirstName'));
   individual.LastName = String.valueOf(personAccount.get('LastName'));
   individual.Email = String.valueOf(personAccount.get('PersonEmail'));
   individual.Address = String.valueOf(personAccount.get('PersonMailingStreet'));
   individual.City = String.valueOf(personAccount.get('PersonMailingCity'));
   individual.State = String.valueOf(personAccount.get('PersonMailingState'));
   individual.PostalCode = String.valueOf(personAccount.get('PersonMailingPostalCode'));
   individual.DOB = String.valueOf(personAccount.get('PersonBirthDate'));
   individual.Phone = String.valueOf(personAccount.get('PersonHomePhone'));

} else {
   String soql = 'Select Id, HealthCloudGA__PrimaryContact__r.FirstName, HealthCloudGA__PrimaryContact__r.LastName, ' +
           'HealthCloudGA__PrimaryContact__r.Email, HealthCloudGA__PrimaryContact__c, BillingStreet, BillingCity, BillingState, BillingPostalCode, HealthCloudGA__PrimaryContact__r.BirthDate, HealthCloudGA__PrimaryContact__r.HomePhone from Account';
   List<sObject> accounts = Database.query(soql);
   SObject personAccount = accounts[0];
   SObject personContact = personAccount.getSObject('HealthCloudGA__PrimaryContact__r');
  
   if (personContact != null) {
       individual.ContactId = String.valueOf(personAccount.get('HealthCloudGA__PrimaryContact__c'));
       individual.DOB = String.valueOf(personContact.get('Birthdate'));
       individual.Phone = String.valueOf(personContact.get('HomePhone'));
       individual.FirstName = String.valueOf(personContact.get('FirstName'));
       individual.LastName = String.valueOf(personContact.get('LastName'));
       individual.Email = String.valueOf(personContact.get('Email'));
   }
   individual.AccountId = String.valueOf(personAccount.get('Id'));
   individual.Address = String.valueOf(personAccount.get('BillingStreet'));
   individual.City = String.valueOf(personAccount.get('BillingCity'));
   individual.State = String.valueOf(personAccount.get('BillingState'));
   individual.PostalCode = String.valueOf(personAccount.get('BillingPostalCode'));
}

The above code is simply for example. Under normal circumstances, the code and queries would be bulkified, selective, and have their inputs sanitized.


Are you setting yourself up for success on the AppExchange by building for scale? If you’re not sure, schedule time for a free consultation session. CodeScience is the first and only Master Navigator PDO. We bring together the experience of over 220 commercial applications brought to market.

App-Chat-Podcast-CTA

Join Our Mailing List