Skip to content

Salesforce Action Launcher Feature Documentation

Overview

Implementation guide for adding Prepare and Present action buttons in Salesforce that launch Pitcher applications using custom settings for domain and connection configuration, with full record context.

Setup Requirements

1. Create Custom Settings (Hierarchical)

Navigate to Setup → Custom Settings → New

Name: Pitcher_Settings__c
Setting Type: Hierarchy
Visibility: Public

Fields:
1. Pitcher_Domain__c (Text, 255) - Example: "demo" (for demo.my.pitcher.com)
2. Connection_ID__c (Text, 255) - Provided by Pitcher

2. Configure Custom Settings

apex
// Via Anonymous Apex or Setup UI
Pitcher_Settings__c settings = Pitcher_Settings__c.getOrgDefaults();
settings.Pitcher_Domain__c = 'your-domain'; // provided by Pitcher
settings.Connection_ID__c = 'your-connection-id'; // provided by Pitcher
upsert settings;

Complete Implementation with Record Context

Apex Controller with Full Context

PitcherActionController.cls

apex
public with sharing class PitcherActionController {
    
    @AuraEnabled(cacheable=true)
    public static Map<String, Object> getPitcherSettings() {
        try {
            Pitcher_Settings__c settings = Pitcher_Settings__c.getOrgDefaults();
            
            // Check if settings exist and are configured
            Boolean isConfigured = settings != null && 
                                 String.isNotBlank(settings.Pitcher_Domain__c) && 
                                 String.isNotBlank(settings.Connection_ID__c);
            
            return new Map<String, Object>{
                'domain' => settings?.Pitcher_Domain__c,
                'connectionId' => settings?.Connection_ID__c,
                'isConfigured' => isConfigured,
                'orgId' => UserInfo.getOrganizationId()
            };
        } catch (Exception e) {
            throw new AuraHandledException('Error retrieving Pitcher settings: ' + e.getMessage());
        }
    }
    
    @AuraEnabled(cacheable=true)
    public static RecordWithInfo getRecordWithInfo(Id recordId) {
        // Get the object type of the record
        String objectType = recordId.getSObjectType().getDescribe().getName();

        String accountIdField = null;
        String accountId = null;

        if (objectType == 'Event') {
            // Handle Event object exceptional case
            accountIdField = 'WhatId';
            SObject record = [SELECT WhatId FROM Event WHERE Id = :recordId LIMIT 1];
            accountId = (String) record.get(accountIdField);
        } else if (objectType == 'Account') {
            // Handle Account object exceptional case
            accountIdField = 'Id';
            accountId = recordId;
        } else {
            // Describe the object to get its fields
            Schema.SObjectType sObjectType = Schema.getGlobalDescribe().get(objectType);
            Map<String, Schema.SObjectField> fieldMap = sObjectType.getDescribe().fields.getMap();

            for (String fieldName : fieldMap.keySet()) {
                Schema.SObjectField field = fieldMap.get(fieldName);
                if (field.getDescribe().getType() == Schema.DisplayType.REFERENCE && 
                    field.getDescribe().getReferenceTo().toString().contains('Account')) {
                    accountIdField = fieldName;
                    break;
                }
            }

            // Query the record to get the AccountId field value
            if (accountIdField != null) {
                String query = 'SELECT ' + accountIdField + ' FROM ' + objectType + ' WHERE Id = :recordId';
                SObject record = Database.query(query);
                accountId = (String) record.get(accountIdField);
            }
        }

        RecordWithInfo result = new RecordWithInfo();
        result.recordId = recordId;
        result.accountIdField = accountIdField;
        result.accountId = accountId;
        result.objectType = objectType;
        return result;
    }

    public class RecordWithInfo {
        @AuraEnabled public Id recordId;
        @AuraEnabled public String accountIdField;
        @AuraEnabled public String accountId;
        @AuraEnabled public String objectType;
    }
}

Lightning Web Component with Full Parameters

actionLauncher.html

html
<template>
    <lightning-card title="Pitcher Actions" icon-name="action:flow">
        <div class="slds-p-around_medium">
            <template if:true={isReady}>
                <div class="slds-button-group-row">
                    <lightning-button 
                        label="Prepare" 
                        variant="brand"
                        onclick={handlePrepare}
                        icon-name="utility:setup"
                        disabled={!isReady}>
                    </lightning-button>
                    <lightning-button 
                        label="Present" 
                        variant="success"
                        onclick={handlePresent}
                        icon-name="utility:call"
                        class="slds-m-left_x-small"
                        disabled={!isReady}>
                    </lightning-button>
                </div>
            </template>
            <template if:false={isConfigured}>
                <div class="slds-text-color_error">
                    Pitcher settings not configured. Please contact your administrator.
                </div>
            </template>
            <template if:true={isLoading}>
                <lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
            </template>
        </div>
    </lightning-card>
</template>

actionLauncher.js

javascript
import { LightningElement, api, wire } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getPitcherSettings from '@salesforce/apex/PitcherActionController.getPitcherSettings';
import getRecordWithInfo from '@salesforce/apex/PitcherActionController.getRecordWithInfo';

export default class ActionLauncher extends LightningElement {
    @api recordId;
    
    // Settings data
    pitcherDomain;
    connectionId;
    orgId;
    isConfigured = false;
    
    // Record data
    recordInfo;
    
    // UI state
    isLoading = true;
    settingsLoaded = false;
    recordLoaded = false;

    @wire(getPitcherSettings)
    wiredSettings({ error, data }) {
        if (data) {
            this.pitcherDomain = data.domain;
            this.connectionId = data.connectionId;
            this.orgId = data.orgId;
            this.isConfigured = data.isConfigured;
            this.settingsLoaded = true;
            this.checkIfReady();
        } else if (error) {
            console.error('Error loading Pitcher settings:', error);
            this.showToast('Error', 'Failed to load Pitcher settings', 'error');
            this.isLoading = false;
        }
    }

    @wire(getRecordWithInfo, { recordId: '$recordId' })
    wiredRecordInfo({ error, data }) {
        if (data) {
            this.recordInfo = data;
            this.recordLoaded = true;
            this.checkIfReady();
        } else if (error) {
            console.error('Error loading record info:', error);
            this.showToast('Error', 'Failed to load record information', 'error');
            this.isLoading = false;
        }
    }

    checkIfReady() {
        if (this.settingsLoaded && this.recordLoaded) {
            this.isLoading = false;
        }
    }

    get isReady() {
        return this.isConfigured && !this.isLoading && this.recordInfo;
    }

    handlePrepare() {
        this.launchPitcher('pitcherai', false);
    }

    handlePresent() {
        this.launchPitcher('start_call', true);
    }

    launchPitcher(action, isStart) {
        if (!this.isReady) {
            this.showToast('Configuration Error', 'System not ready or configured', 'error');
            return;
        }

        // Build comprehensive parameters matching Pitcher's expected format
        const params = new URLSearchParams({
            PostcallId: this.recordId || '',
            AccountId: this.recordInfo.accountId || '',
            Type: this.recordInfo.objectType || '',
            orgId: this.orgId,
            timestamp: Date.now().toString(),
            start: isStart.toString()
        });

        // Build redirect URL following Pitcher format
        const redirectUrl = `https://${this.pitcherDomain}.my.pitcher.com/?connection=${this.connectionId}&next=/${action}?${params.toString()}`;
        
        console.log('Launching Pitcher with URL:', redirectUrl);
        
        window.open(
            redirectUrl,
            isStart ? 'pitcherPresent' : 'pitcherPrepare',
            'width=1600,height=900,toolbar=no,location=no,menubar=no'
        );

        const actionLabel = isStart ? 'Presentation' : 'Preparation';
        this.showToast('Launching', `Opening Pitcher ${actionLabel}...`, 'success');
    }

    showToast(title, message, variant) {
        this.dispatchEvent(
            new ShowToastEvent({
                title: title,
                message: message,
                variant: variant
            })
        );
    }
}

actionLauncher.js-meta.xml

xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>59.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Pitcher Action Launcher</masterLabel>
    <description>Launch Pitcher AI and presentation tools with full context</description>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
        <target>lightning__UtilityBar</target>
    </targets>
</LightningComponentBundle>

Alternative: Visualforce Implementation with Full Context

PitcherActionPage.page

html
<apex:page controller="PitcherActionPageController" showHeader="false" sidebar="false">
    <script>
        var pitcherDomain = '{!JSENCODE(pitcherDomain)}';
        var connectionId = '{!JSENCODE(connectionId)}';
        var orgId = '{!JSENCODE(orgId)}';
        var recordId = '{!$CurrentPage.parameters.recordId}';
        var recordInfo = null;
        
        // Load record info on page load using vanilla JavaScript
        window.onload = function() {
            if (recordId) {
                Visualforce.remoting.Manager.invokeAction(
                    '{!$RemoteAction.PitcherActionPageController.getRecordInfo}',
                    recordId,
                    function(result, event) {
                        if (event.status) {
                            recordInfo = result;
                            enableButtons();
                        } else {
                            console.error('Failed to load record info:', event.message);
                        }
                    }
                );
            }
        };
        
        function enableButtons() {
            document.getElementById('prepareBtn').disabled = false;
            document.getElementById('presentBtn').disabled = false;
        }
        
        function launchPrepare() {
            launchPitcher('pitcherai', false);
        }
        
        function launchPresent() {
            launchPitcher('start_call', true);
        }
        
        function buildQueryString(params) {
            var queryString = '';
            for (var key in params) {
                if (params.hasOwnProperty(key)) {
                    if (queryString.length > 0) queryString += '&';
                    queryString += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
                }
            }
            return queryString;
        }
        
        function launchPitcher(action, isStart) {
            if (!pitcherDomain || !connectionId) {
                alert('Pitcher settings not configured');
                return;
            }
            
            var params = buildQueryString({
                PostcallId: recordId || '',
                AccountId: recordInfo ? recordInfo.accountId : '',
                Type: recordInfo ? recordInfo.objectType : '',
                orgId: orgId,
                timestamp: Date.now(),
                start: isStart
            });
            
            var redirectUrl = 'https://' + pitcherDomain + '.my.pitcher.com/?connection=' + 
                            connectionId + '&next=/' + action + '?' + params;
            
            window.open(redirectUrl, isStart ? 'pitcherPresent' : 'pitcherPrepare', 
                       'width=1600,height=900');
        }
    </script>
    
    <div style="padding: 20px; text-align: center;">
        <button id="prepareBtn" onclick="launchPrepare()" disabled="true"
                style="padding: 10px 20px; margin: 5px; background: #0070d2; color: white; 
                       border: none; border-radius: 4px; cursor: pointer;">
            Prepare
        </button>
        <button id="presentBtn" onclick="launchPresent()" disabled="true"
                style="padding: 10px 20px; margin: 5px; background: #04844b; color: white; 
                       border: none; border-radius: 4px; cursor: pointer;">
            Present
        </button>
    </div>
</apex:page>

PitcherActionPageController.cls

apex
public with sharing class PitcherActionPageController {
    
    public String pitcherDomain { get; private set; }
    public String connectionId { get; private set; }
    public String orgId { get; private set; }
    
    public PitcherActionPageController() {
        loadPitcherSettings();
        orgId = UserInfo.getOrganizationId();
    }
    
    private void loadPitcherSettings() {
        try {
            Pitcher_Settings__c settings = Pitcher_Settings__c.getOrgDefaults();
            if (settings != null) {
                pitcherDomain = settings.Pitcher_Domain__c;
                connectionId = settings.Connection_ID__c;
            }
        } catch (Exception e) {
            ApexPages.addMessage(new ApexPages.Message(
                ApexPages.Severity.ERROR, 
                'Error loading Pitcher settings: ' + e.getMessage()
            ));
        }
    }
    
    @RemoteAction
    public static RecordWithInfo getRecordInfo(Id recordId) {
        return PitcherActionController.getRecordWithInfo(recordId);
    }
    
    public class RecordWithInfo {
        public Id recordId;
        public String accountIdField;
        public String accountId;
        public String objectType;
    }
}

Test Class with Full Coverage

apex
@isTest
public class PitcherActionControllerTest {
    
    @testSetup
    static void setup() {
        // Create test custom settings
        Pitcher_Settings__c settings = new Pitcher_Settings__c();
        settings.Pitcher_Domain__c = 'test-domain';
        settings.Connection_ID__c = 'test-connection-123';
        insert settings;
        
        // Create test Account
        Account testAccount = new Account(Name = 'Test Account');
        insert testAccount;
        
        // Create test Opportunity
        Opportunity testOpp = new Opportunity(
            Name = 'Test Opportunity',
            AccountId = testAccount.Id,
            StageName = 'Prospecting',
            CloseDate = Date.today().addDays(30)
        );
        insert testOpp;
        
        // Create test Event
        Event testEvent = new Event(
            Subject = 'Test Event',
            WhatId = testAccount.Id,
            StartDateTime = DateTime.now(),
            EndDateTime = DateTime.now().addHours(1)
        );
        insert testEvent;
    }
    
    @isTest
    static void testGetPitcherSettings() {
        Test.startTest();
        Map<String, Object> result = PitcherActionController.getPitcherSettings();
        Test.stopTest();
        
        System.assertEquals('test-domain', result.get('domain'));
        System.assertEquals('test-connection-123', result.get('connectionId'));
        System.assertEquals(true, result.get('isConfigured'));
        System.assertNotEquals(null, result.get('orgId'));
    }
    
    @isTest
    static void testGetRecordWithInfoOpportunity() {
        Opportunity opp = [SELECT Id, AccountId FROM Opportunity LIMIT 1];
        
        Test.startTest();
        PitcherActionController.RecordWithInfo result = 
            PitcherActionController.getRecordWithInfo(opp.Id);
        Test.stopTest();
        
        System.assertEquals(opp.Id, result.recordId);
        System.assertEquals('Opportunity', result.objectType);
        System.assertEquals(opp.AccountId, result.accountId);
        System.assertEquals('AccountId', result.accountIdField);
    }
    
    @isTest
    static void testGetRecordWithInfoAccount() {
        Account acc = [SELECT Id FROM Account LIMIT 1];
        
        Test.startTest();
        PitcherActionController.RecordWithInfo result = 
            PitcherActionController.getRecordWithInfo(acc.Id);
        Test.stopTest();
        
        System.assertEquals(acc.Id, result.recordId);
        System.assertEquals('Account', result.objectType);
        System.assertEquals(acc.Id, result.accountId);
        System.assertEquals('Id', result.accountIdField);
    }
    
    @isTest
    static void testGetRecordWithInfoEvent() {
        Event evt = [SELECT Id, WhatId FROM Event LIMIT 1];
        
        Test.startTest();
        PitcherActionController.RecordWithInfo result = 
            PitcherActionController.getRecordWithInfo(evt.Id);
        Test.stopTest();
        
        System.assertEquals(evt.Id, result.recordId);
        System.assertEquals('Event', result.objectType);
        System.assertEquals(evt.WhatId, result.accountId);
        System.assertEquals('WhatId', result.accountIdField);
    }
}

URL Format with Complete Parameters

Prepare URL Example

https://demo.my.pitcher.com/?connection=abc123&next=/pitcherai?PostcallId=0061234567890AB&AccountId=0011234567890AB&Type=Opportunity&orgId=00D1234567890AB&timestamp=1704067200000&start=false

Present URL Example

https://demo.my.pitcher.com/?connection=abc123&next=/start_call?PostcallId=0061234567890AB&AccountId=0011234567890AB&Type=Opportunity&orgId=00D1234567890AB&timestamp=1704067200000&start=true

URL Parameters Reference

ParameterDescriptionExample
PostcallIdCurrent record ID0061234567890AB
AccountIdRelated Account ID0011234567890AB
TypeObject type of current recordOpportunity, Lead, Account, Event
orgIdSalesforce Organization ID00D1234567890AB
timestampCurrent timestamp in milliseconds1704067200000
startBoolean for Present actiontrue/false

Deployment Checklist

  • [ ] Create Pitcher_Settings__c custom setting
  • [ ] Deploy PitcherActionController.cls with test class
  • [ ] Deploy Lightning Web Component files
  • [ ] Configure custom settings with Pitcher domain and connection ID
  • [ ] Add Remote Site Settings for *.my.pitcher.com
  • [ ] Add CSP Trusted Sites for *.my.pitcher.com
  • [ ] Add component to page layouts
  • [ ] Test with different record types (Account, Opportunity, Event)
  • [ ] Verify popup blocker settings
  • [ ] Validate URL parameters in browser developer tools

Security Considerations

  1. Field Level Security: Ensure users have read access to:

    • Pitcher_Settings__c custom setting
    • Account lookup fields on objects
    • Record IDs they're accessing
  2. Apex Security: Classes use with sharing to respect record access

  3. Remote Sites: Configure for HTTPS only

  4. Parameter Validation: All parameters are encoded to prevent injection

Troubleshooting Guide

Debug JavaScript Console

javascript
// Add to LWC for debugging
connectedCallback() {
    console.log('=== Pitcher Debug Info ===');
    console.log('Org ID:', this.orgId);
    console.log('Record ID:', this.recordId);
    console.log('Pitcher Domain:', this.pitcherDomain);
    console.log('Connection ID:', this.connectionId);
    console.log('Record Info:', this.recordInfo);
    console.log('Is Configured:', this.isConfigured);
    console.log('Is Ready:', this.isReady);
}

Common Issues and Solutions

  1. Buttons Disabled

    • Check if custom settings are populated
    • Verify record ID is being passed
    • Check browser console for errors
  2. Invalid Integration Error

    • Verify connection ID with Pitcher support
    • Check domain spelling and format
    • Test URL directly in browser
  3. Missing Account ID

    • Verify object has Account lookup field
    • Check field-level security
    • Review special cases (Event, Account objects)