Integrate Salesforce with Google MAP API

Addresses are very important when it comes to business. It becomes more important when we are running high profile business and capturing the correct address information is utmost important. When we go for any address lookup, what best it can be if the search is able to serve for auto-fill and auto-predict. Google Map API, specially the Google Place API allows us to get recommendations based on the user input for the address search and the place details for selected recommendation. This information can then be stored for our business purpose. 
This blog explains how we can use the Google Place API for recommendations and place details. We will use Account to show the demo on how can we use this address tool for capturing the address details for the Billing Address. This can then be extended to any custom implementation and any custom object.

Steps involved in the setup:

  1. API Setup on Google G-Suite Account.
  2. Create Apex Class: To call the Google Map API to get recommendation and place details.
  3. Create Lightning Web Component for user experience.

API Setup:

  1. To use the Google API, we need to have our account set up on Google G-Suite or Google Cloud. Follow the steps as explained under Setup in Cloud Console.
  2. Once you have your project ready and linked with the billing account, you project is ready to use. You will need to get an API key to be used in order to consume the Google API services. Read Using API Key documentation to get an API Key.
  3. Make sure you enable the Google Places API.
  4. This Google Place API has two endpoints
    • Recommendations: This helps us to get the address recommendation as the user types in for the address search. Use following GET service from Google Place API to get recommendations. 
      https://maps.googleapis.com/maps/api/place/autocomplete/json?input=SEARCH_TEXT&key=YOUR_API_KEY

      Here SEARCH_TEXT is user input for address search and YOUR_API_KEY is the API key that you get as in Step 2. 

    • Place Details: This helps us to get the place details with full information. Use following GET service from Google Place API to get place details
      https://maps.googleapis.com/maps/api/place/details/json?placeid=PLACE_ID&key=YOUR_API_KEY

      Here PLACE_ID is selected place from available options for address recommendations and YOUR_API_KEY is the API key that you get as in Step 2.

Create APEX Class:

Create an APEX class that will call the Google Map API for address details. In order to consume the Google Place API in our Salesforce application, we need to follow below steps.
  1. Lightning components cannot reach out to Places API directly. Google doesn’t allow CORS to their endpoints – so we simply cannot do this. Nor would we want to – because if we do this via JavaScript – we will be exposing our keys. So, we need the server to do the calls.
  2. We need to set up remote site setting to make a callout to Google Maps API.
    1. Remote Site Name : GoogleMapAPI
    2. Remote Site URL: https://maps.googleapis.com
  3. Create APEX class named : AddressSearchController
    public class AddressSearchController {
        //Method to call Google Map API and fetch the address recommendations 
        @AuraEnabled
        public static String getAddressRecommendations(String searchText){
            String apiKey = '';//YOUR_API_KEY
            String result = null;
            try{
                if(searchText != null){
                    String apiUrl = 'https://maps.googleapis.com/maps/api/place/autocomplete/json?input=' + searchText.replace(' ', '%20') + '&key=' + apiKey; 
                    HttpRequest req = new HttpRequest();
                    req.setMethod('GET');
                    req.setEndpoint(apiUrl);
                    Http http = new Http();
                    HttpResponse res = http.send(req);
                    Integer statusCode = res.getStatusCode();
                    if(statusCode == 200){
                        result = res.getBody();
                    }
                }
            } catch(exception e){
                System.debug(e.getMessage());
            }
            return result;
        }
        
        //Method to call Google Map API and fetch the address details by placeId 
        @AuraEnabled
        public static String getAddressDetailsByPlaceId(String placeId){
            String apiKey = '';//YOUR_API_KEY
            String result = null;
            try{
                if(placeId != null){
                    String apiUrl = 'https://maps.googleapis.com/maps/api/place/details/json?placeid=' + placeId.replace(' ', '%20') + '&key=' + apiKey; 
                    HttpRequest req = new HttpRequest();
                    req.setMethod('GET');
                    req.setEndpoint(apiUrl);
                    Http http = new Http();
                    HttpResponse res = http.send(req);
                    Integer statusCode = res.getStatusCode();
                    if(statusCode == 200){
                        result = res.getBody();
                    }
                }
            } catch(exception e){
                System.debug(e.getMessage());
            }
            return result;
        }
        
        //Method to update the address on Account
        @AuraEnabled
        public static void updateAddressDetails(String jsonAddress) {
            Address addressDetail = (Address)JSON.deserialize(jsonAddress, Address.Class);
            if(addressDetail != null && !String.isBlank(addressDetail.Id)) {
                Account acc = new Account();
                acc.Id = addressDetail.Id;
                acc.BillingState = addressDetail.state;
                acc.BillingCity = addressDetail.city;
                acc.BillingCountry = addressDetail.country;
                acc.BillingPostalCode = addressDetail.postalCode;
                acc.BillingStreet = addressDetail.streetNumber;
                update acc;
            }
        }
        
        public class Address {
            public String Id {get; set;}
            public String city {get; set;}
            public String country {get; set;}
            public String state {get; set;}
            public String postalCode {get; set;}
            public String subLocal2 {get; set;}
            public String subLocal1 {get; set;}
            public String streetNumber {get; set;}
            public String route {get; set;}
        }
    }

Create Lightning Web Component:

Create a lightning web component named addressSearch for a user experience which will allow a search box to enter for the address search. This will read the APEX class created above and provide the recommendations for the place selection. Once the place is selected, the place details API will get called and all related information is then mapped to Billing Address on Account record.
addressSearch.html
<template>
    <!-- Address Information Header -->
    <div class="slds-grid slds-wrap">
        <div class="slds-col">
            <div class="slds-page-header">
                <h1 class="slds-list_horizontal">
                    <span class="slds-page-header__title slds-truncate" title="Address Information"
                        style="width: 100%">Address
                        Information</span>
                    <lightning-button class="slds-float_right" label="Edit" title="Edit" slot="actions"
                        onclick={handleModal}></lightning-button>
                </h1>
            </div>
        </div>
    </div>
    <!-- Address Information Details -->
    <div class="addressInformation">
        <lightning-record-edit-form record-id={recordId} object-api-name={objectApiName}>
            <lightning-layout multiple-rows>
                <template for:each={fieldApiNames} for:item="fieldApiName">
                    <lightning-layout-item key={fieldApiName} flexibility="auto" padding="around-small" size="6"
                        large-device-size="6" medium-device-size="6">
                        <lightning-output-field class="slds-form-element_readonly" field-name={fieldApiName}>
                        </lightning-output-field>
                    </lightning-layout-item>
                </template>
            </lightning-layout>
        </lightning-record-edit-form>
    </div>
    <!-- Address Search Modal-->
    <div class="openModal" if:true={openModal}>
        <section role="dialog" tabindex="-1" class="slds-modal slds-fade-in-open" aria-labelledby="modal-heading-01"
            aria-modal="true" aria-describedby="modal-content-id-1">
            <div class="slds-modal__container">
                <header class="slds-modal__header">
                    <button class="slds-button slds-button_icon slds-modal__close slds-button_icon-inverse"
                        title="Close" onclick={closeModal}>
                        <lightning-icon icon-name="utility:close" alternative-text="close" variant="inverse"
                            size="small">
                        </lightning-icon>
                        <span class="slds-assistive-text">Close</span>
                    </button>
                    <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate">Address Search</h2>
                </header>
                <div class="slds-modal__content slds-var-p-around_medium" id="modal-content-id-1">
                    <lightning-input type="search" variant="label-hidden" class="searchAddress" name="searchAddress"
                        placeholder="Search Address.." onchange={handleChange} value={selectedAddress}>
                    </lightning-input>
                    <!-- Address Recommendations -->
                    <div if:true={hasRecommendations}>
                        <div class="address-recommendations" role="listbox">
                            <ul class="slds-listbox slds-listbox_vertical slds-dropdown slds-dropdown_fluid"
                                role="presentation">
                                <template for:each={addressRecommendations} for:item="addressRecommendation">
                                    <li key={addressRecommendation} role="presentation"
                                        onclick={handleAddressRecommendationSelect}
                                        data-value={addressRecommendation.place_id} class="slds-listbox__item">
                                        <span
                                            class="slds-media slds-listbox__option slds-listbox__option_entity slds-listbox__option_has-meta"
                                            role="option">
                                            <span class="slds-media__body slds-m-left_xx-small slds-m-bottom_xx-small">
                                                <div class="slds-grid slds-m-bottom_small">
                                                    <div class="slds-col slds-size_1-of-10">
                                                        <lightning-button-icon size="medium" icon-name="utility:checkin"
                                                            class="slds-input__icon" variant="bare">
                                                        </lightning-button-icon>
                                                    </div>
                                                    <div class="slds-m-left_medium slds-col slds-size_8-of-10">
                                                        <span
                                                            class="slds-listbox__option-text slds-listbox__option-text_entity"><b>{addressRecommendation.main_text}</b></span>
                                                        <span
                                                            class="slds-listbox__option-text slds-listbox__option-text_entity slds-m-top_xxx-small">{addressRecommendation.secondary_text}</span>
                                                    </div>
                                                    <div class="slds-col slds-size_1-of-10"></div>
                                                </div>
                                            </span>
                                        </span>
                                    </li>
                                </template>
                            </ul>
                        </div>
                    </div>
                </div>
                <footer class="slds-modal__footer">
                    <lightning-button class="slds-button" label="Cancel" onclick={closeModal} variant="neutral">
                    </lightning-button>
                    <lightning-button class="slds-button" label="Save" onclick={saveAddress} variant="brand">
                    </lightning-button>
                </footer>
            </div>
        </section>
        <div class="slds-backdrop slds-backdrop_open"></div>
    </div>
    <!-- Lightning Spinner -->
    <div class="showSpinner" if:true={showSpinner}>
        <lightning-spinner alternative-text="Loading" variant="brand"></lightning-spinner>
    </div>
</template>
addressSearch.js
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import getAddressRecommendations from '@salesforce/apex/AddressSearchController.getAddressRecommendations';
import getAddressDetailsByPlaceId from '@salesforce/apex/AddressSearchController.getAddressDetailsByPlaceId';
import updateAddressDetails from '@salesforce/apex/AddressSearchController.updateAddressDetails';

export default class AddressSearch extends LightningElement {
    @api recordId;
    @api objectApiName;
    openModal = false;
    showSpinner = false;
    fieldApiNames = ['BillingStreet', 'BillingCity', 'BillingState', 'BillingPostalCode', 'BillingCountry'];
    addressRecommendations;
    selectedAddress = '';
    addressDetail = {};

    get hasRecommendations() {
        return (this.addressRecommendations !== null && this.addressRecommendations.length);
    }
    
    handleChange(event) {
        event.preventDefault();
        let searchText = event.target.value;
        if (searchText) this.getAddressRecommendations(searchText);
        else this.addressRecommendations = [];
    }

    getAddressRecommendations(searchText) {
        getAddressRecommendations({ searchText: searchText })
            .then(response => {
                response = JSON.parse(response);
                let addressRecommendations = [];
                response.predictions.forEach(prediction => {
                    addressRecommendations.push({
                        main_text: prediction.structured_formatting.main_text,
                        secondary_text: prediction.structured_formatting.secondary_text,
                        place_id: prediction.place_id,
                    });
                });
                this.addressRecommendations = addressRecommendations;
            }).catch(error => {
                console.log('error : ' + JSON.stringify(error));
            });
    }

    handleAddressRecommendationSelect(event) {
        event.preventDefault();
        let placeId = event.currentTarget.dataset.value;
        this.addressRecommendations = [];
        this.selectedAddress = '';
        getAddressDetailsByPlaceId({ placeId: placeId })
            .then(response => {
                response = JSON.parse(response);
                response.result.address_components.forEach(address => {
                    let type = address.types[0];
                    switch (type) {
                        case 'locality':
                            this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
                            this.addressDetail.city = address.long_name;
                            break;
                        case 'country':
                            this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
                            this.addressDetail.country = address.long_name;
                            break;
                        case 'administrative_area_level_1':
                            this.selectedAddress = this.selectedAddress + ' ' + address.short_name;
                            this.addressDetail.state = address.short_name;
                            break;
                        case 'postal_code':
                            this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
                            this.addressDetail.postalCode = address.long_name;
                            break;
                        case 'sublocality_level_2':
                            this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
                            this.addressDetail.subLocal2 = address.long_name;
                            break;
                        case 'sublocality_level_1':
                            this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
                            this.addressDetail.subLocal1 = address.long_name;
                            break;
                        case 'street_number':
                            this.selectedAddress = this.selectedAddress + ' ' + address.long_name;
                            this.addressDetail.streetNumber = address.long_name;
                            break;
                        case 'route':
                            this.selectedAddress = this.selectedAddress + ' ' + address.short_name;
                            this.addressDetail.route = address.short_name;
                            break;
                        default:
                            break;
                    }
                });
            })
            .catch(error => {
                console.log('error : ' + JSON.stringify(error));
            });
    }
    
    handleModal(event) {
        event.preventDefault();
        this.openModal = true;
        this.addressRecommendations = [];
    }

    closeModal(event) {
        event.preventDefault();
        this.openModal = false;
        this.addressRecommendations = [];
    }

    saveAddress(event) {
        event.preventDefault();
        this.openModal = false;
        this.showSpinner = true;
        this.addressDetail.Id = this.recordId;
        updateAddressDetails({ jsonAddress: JSON.stringify(this.addressDetail) })
            .then(() => {
                const event = new ShowToastEvent({
                    title: 'Success',
                    message: 'Account address is updatd successfully.',
                    variant: 'success'
                });
                this.dispatchEvent(event);
                this.showSpinner = false;
            })
            .catch(error => {
                const event = new ShowToastEvent({
                    title: 'Error',
                    message: 'An error has occured in saving the address.',
                    variant: 'error'
                });
                this.dispatchEvent(event);
                console.log('error : ' + JSON.stringify(error));
                this.showSpinner = false;
            });
        
    }
}
addressSearch.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
</LightningComponentBundle>

Demo:

Edit the Account Record page, and drag-drop your newly created component addressSearch below the details section.


If you like this blog content and find inciteful, please comment and let me know. 

Comments

  1. Thank you for sharing your knowlwdge :)

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. This is really cool but I am having trouble restricting access to the Google api key based on http referrer or IP address. The apex call does not seem to come from my SF domain. Anyone have suggestions?

    ReplyDelete

Post a Comment