Travis Cucore

Contact Me Integration With Salesforce

Summary

Note: You'll notice the "Contact Me" button discussed in this post is no longer present on my website. That's because I opted to leverage my M365 Bookings page instead as it saves me alot of back and forth on scheduling while I look for my next role.

How many times have you implemented a "contact me" form on an external website using Salesforce's email-to-case or used some other back-door pattern to get the job done? I'm on a misson to demonstrate that clean and maintainable Salesforce integrations are not only possible but within reach. It's time we look beyond the marketing materials and share our success stories when we step off platform and do something cool.

Salesforce offers an extensive set of APIs. So extensive, in fact, that you may be surprised by the number available. I recommend bookmarking the Salesforce API Library. It's a resource I regularly use to determine which tools are at my disposal for solving client problems.

For this particular bit of functionality, I utilized the standard CRUD endpoint for a custom object I created named ContactRequest. For authentication, the JWT bearer flow is my go-to method. All API transactions are facilitated by a connected app in Salesforce, leveraging the jsforce node package, and the node-salesforce-jwt package.

The code that manages the endpoint, handles authentication, and orchestrates traffic to and from Salesforce for operations is executed server-side. This ensures sensitive information is not exposed to the user's browser. Endpoint access is limited to localhost, as the server hosting the website also hosts the endpoint.

Things We'll Need To Do

  • Create a connected app in Salesforce: Refer to the CONNECTED APP DOCUMENTATION if you're unfamiliar with this process. Scopes and authentication requirements will vary based on your needs.

  • Generate a self-signed certificate: Follow THIS GUIDE if you need instructions.

  • Upload the certificate to the connected app: This step should be straightforward if you've completed the previous tasks.

  • Set up the API endpoint:

    1. Create a new folder at project-root/api/<filename>.js.
    2. Use jsforce and node-salesforce-jwt packages within <filename>.js.
    3. Configure your API to reference your *.key file for the JWT grant.
  • Create and integrate a "contact me" form: Embed the form into your webpage header for easy access.

Here's the form I put together for this website though it's no longer implemented.

import * as React from 'react'
import { TextBox, TextArea, Button } from './components'
import Spinner from './spinner';

const ContactForm = ( { className } ) => {

const DEFAULT_FOOTER_TEXT = 'Thanks for reaching out!  I look forward to hearing from you :)'
const ERROR_FOOTER_TEXT_TEMPLATE = `Well, this is emberrassing.  I've logged the details of this event and created a task for myself to address it.  Thanks for finding the problem.`;
  const REQUEST_SENT_FOOTER_TEXT = 'Sending your request to Salesforce using their standard OOTB REST API';
  const REQUEST_ERROR_FOOTER_TEXT = `Your request was sent, but the server returned an exception.  I've logged the relivent information (no PII was collected) and will adress it as soon as I can.`
  const SUCCESS_FOOTER_TEXT = `All done!  You'll get an email confirming the details of your request.  I'll get back to you as soon as I can.  This window will close in a couple of seconds.`;
  const FORM_ERROR_FOOTER_TEXT = `The form did not validate.  The problem field should be telling you what to fix.`

  const [footerText, setFooterText] = React.useState(DEFAULT_FOOTER_TEXT);
  const [renderSpinner, setRenderSpinner] = React.useState(false);

  const buildFooterFromTextFromTemplate = (template, error) => {
   return `${template} \n ${JSON.stringify(error)}`;
  }

  const handleClose = () => {
    //close the dialog modal with the contact me form
    document.getElementById('contact-me').close();
  }

  const handleSubmit = (event) => {
    
    setRenderSpinner(true);
    setFooterText(REQUEST_SENT_FOOTER_TEXT);

    const contactMeForm = document.forms.contactMeForm;

    if(contactMeForm.checkValidity()){
      event.preventDefault();

      const formData = new FormData(contactMeForm);

      //Append data not on form.
      formData.append('Status__c', "New");
      formData.append('Origin__c', window.location.origin);

      const headers = new Headers();
      headers.append("sObjectType", "Contact_Request__c");
  
      const REQUEST_OPTIONS = {
        method: "POST", // *GET, POST, PUT, DELETE, etc.
        headers: headers,
        body: formData
      }
  
      fetch(`${document.location.origin}/api/salesforce`, REQUEST_OPTIONS)
      .then(response => { //Handle response

       if(response.status === 200){

          //Handle the problem.  Handle it in a seperate method.
         setFooterText(SUCCESS_FOOTER_TEXT);
          handleClose();
        }else{
          //ToDo: handle less than ideal responses.
          setFooterText(REQUEST_ERROR_FOOTER_TEXT);
        }
      })
      .then(result => {
      setRenderSpinner(false);
      })
      .catch(error => { //Handle exceptions
       setFooterText(buildFooterFromTextFromTemplate(ERROR_FOOTER_TEXT_TEMPLATE));
        setRenderSpinner(false);
        //ToDo: Log an exception to Salesforce.
      });
    }else{  //Let the user know they missed a field that's required, or they have not met some field validation.
     setFooterText(FORM_ERROR_FOOTER_TEXT);
      setRenderSpinner(false);
    }
  }

  return (
    <div className={className}>
      <form id={`contactMeForm`}>
        <h1 className='text-title text-center'>Contact Me</h1>
        <fieldset className={`w-full grid grid-cols-2 place-items-center gap-5 max-h-full`}>
          <TextBox label={`First Name`} name={`First_Name__c`} id={`first-name-field`}
              className={`w-full col-span-1`}
              type="text"
              required={true}/>
          <TextBox label={`Last Name`} name={`Last_Name__c`} id={`last-name-field`}
            className={`w-full col-span-1`}
            type="text"
            required={true}/>
          <TextBox label={`Email Address`} name={`Email_Address__c`} id={`email-address-field`}
            className={`w-full col-span-2`}
            type="email" 
            patternMismatchMessage={process.env.EMAIL_INVALID_MESSAGE}
            matchPattern={process.env.EMAIL_REGEX}
            required={true}/>
          <TextArea label={`Message`} name={`Request_Details__c`} id={`message-text-area`} 
            className={`h-fit w-full col-span-2`} 
            resize="none"
            required={true}/>
        </fieldset>
        <div className={`grid grid-cols-12 gap-3 items-center content-center`}>
          <p className={`col-span-10 text-label`}>{footerText}</p>
          <Spinner className={`${(renderSpinner ? 'col-span-1' : 'hidden')}`} size="md" color="#0fabd4" fillColor="#0fabd4"/>
          <div className={`${(renderSpinner ? 'hidden' : 'col-span-2 grid grid-rows-2 gap-3')}`}>
            <Button className={``} label="Cancel" type="cancel" clickHandler={ handleClose }/>
            <Button className={``} label="Submit" type="submit" formId={'contactMeForm'} clickHandler={ handleSubmit }/>
          </div>
        </div>
      </form>
    </div>

  )
}

export default ContactForm

Once you have steps 1-3 in place, it's time to focus on setting up your API endpoint and integrating client-side code with your UI, as detailed above.

Here's an overview of what the server-side file might look like:

import * as jsforce from 'jsforce';
import * as nsj from 'node-salesforce-jwt';
import * as fs from 'fs';

require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
});

// Salesforce API function definition 
const SalesforceApi = (req, res) => {
  // Set up response header
  res.setHeader('Access-Control-Allow-Origin', '*');
  // ... (remaining code)
};

export default SalesforceApi;

This code snippet is just an illustration—your actual implementation may vary. If you have any questions or would like to discuss this further, feel free to reach out. In the meantime, make sure to adjust the paths point to your API and key locations:

fetch(`${document.location.origin}/api/salesforce`, REQUEST_OPTIONS);
const privateKey = fs.readFileSync('./PATH/TO/server.key', 'utf8');

Integrating this with your client-side code and UI should be your next step. Using environment variables for sensitive data is also a wise move.

I hope you found this helpful, or at least entertaining. I don't post very often, but check back every once in a while. Sometimes I get bored and decided to spend some time putting one of these posts together, and I've got alot of ideas for content.