Skip to content

Instantly share code, notes, and snippets.

@amitastreait
Created October 19, 2023 07:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save amitastreait/646d6de0829ded8b294a5132936192ef to your computer and use it in GitHub Desktop.
Save amitastreait/646d6de0829ded8b294a5132936192ef to your computer and use it in GitHub Desktop.
<template>
<lightning-card variant="Narrow" >
<lightning-button if:true={messageList} variant="border-filled"
label="Chat with another customer" onclick={handleAnOtherChat} slot="actions">
</lightning-button>
<lightning-spinner alternative-text="Loading" size="small" if:true={isSpinner}></lightning-spinner>
<div class="slds-p-horizontal_small">
<section role="log" class="slds-chat" if:false={messageList}>
<lightning-input type="tel"
message-when-value-missing="Please provide a valid contact no to chat with"
required variant="label-hidden" name="Phone"
onchange={handlePhoneChange}
placeholder="Enter Customer Phone Number and click next">
</lightning-input>
<br/>
<lightning-button variant="brand" label="Chat with Customer" title="Chat with Customer" onclick={handlelistAllMessageByCustomer}></lightning-button>
</section>
<section role="log" class="messageContent slds-chat slds-scrollable" if:true={messageList} style="height: 350px;">
<ul class="slds-chat-list">
<template if:true={messageList} for:each={messageList} for:item="message" for:index="index">
<!-- Message From Customer -->
<li class="slds-chat-listitem slds-chat-listitem_inbound" key={message.Id} if:false={message.Outgoing__c}>
<div class="slds-chat-message" if:true={message.MessageContent__c}>
<span aria-hidden="true" class="slds-avatar slds-avatar_circle slds-chat-avatar">
<abbr class="slds-avatar__initials slds-avatar__initials_inverse" title={message.CustomerName__c}>AM</abbr>
</span>
<div class="slds-chat-message__body">
<div class="slds-chat-message__text slds-chat-message__text_inbound">
<span>
<lightning-formatted-rich-text value={message.MessageContent__c}></lightning-formatted-rich-text>
</span>
</div>
<div class="slds-chat-message__meta" aria-label={message.areaLabel}>{message.CustomerName__c} • <lightning-formatted-date-time value={message.CreatedDate} hour="2-digit"></lightning-formatted-date-time></div>
</div>
</div>
</li>
<!-- Message From Salesforce -->
<li class="slds-chat-listitem slds-chat-listitem_outbound" key={message.Id} if:true={message.Outgoing__c}>
<div class="slds-chat-message" if:true={message.MessageContent__c}>
<!-- <span aria-hidden="true" class="slds-avatar slds-avatar_circle slds-chat-avatar">
<abbr class="slds-avatar__initials slds-avatar__initials_inverse" title={message.AgentName__c}>AM</abbr>
</span> -->
<div class="slds-chat-message__body">
<div class="slds-chat-message__text slds-chat-message__text_outbound">
<span>
<lightning-formatted-rich-text value={message.MessageContent__c}></lightning-formatted-rich-text>
</span>
</div>
<div class="slds-chat-message__meta" aria-label={message.areaLabel}>{message.AgentName__c} • <lightning-formatted-date-time value={message.CreatedDate} hour="2-digit"></lightning-formatted-date-time></div>
</div>
</div>
</li>
</template>
</ul>
</section>
<div if:true={chatEnabled}>
<span class="slds-icon_container slds-icon-utility-end_chat slds-chat-icon">
<svg class="slds-icon slds-icon_x-small slds-icon-text-default" aria-hidden="true">
<use xlink:href="/apexpages/slds/latest/assets/icons/utility-sprite/svg/symbols.svg#chat"></use>
</svg>
</span>
<p>
<lightning-button-icon onclick={handleSendMessage} class="btnIconOverlay" icon-name="utility:send">
</lightning-button-icon>
<lightning-input message-when-value-missing="Please type a valid messgae before sending it"
required value={chatMessage} type="text" class="chat-input"
variant="label-hidden" onchange={handleChatChange} name="Message"
placeholder="type here...">
</lightning-input>
</p>
</div>
</div>
<div slot="footer">
Powered by <a href="https://www.pantherschools.com/" target="_blank" aria-label="Panther Schools">@PantherSchools.com</a>
</div>
</lightning-card>
</template>
import { api, LightningElement, track } from 'lwc';
import listAllMessageByCustomer from '@salesforce/apex/WhatsAppLWCService.listAllMessageByCustomer';
import sendTextMessage from '@salesforce/apex/WhatsAppLWCService.sendTextMessage';
import getSingleMessage from '@salesforce/apex/WhatsAppLWCService.getSingleMessage';
import { subscribe, onError } from 'lightning/empApi';
export default class WhatsAppChatComponent extends LightningElement {
@api recordId;
@track messageList;
@track errors;
channelName = '/event/WAMessageEvent__e';
subscription = {};
chatMessage
customerNo;
isSpinner = false;
chatEnabled = false;
handleChatChange(event){
event.preventDefault();
this.chatMessage = event.target.value;
}
connectedCallback(){
this.registerErrorListener();
this.handleSubscribe();
}
handlelistAllMessageByCustomer(event){
event.preventDefault();
if(!this.handleValidate()){
return;
}
this.isSpinner = true;
listAllMessageByCustomer({
customerPhone : this.customerNo
})
.then(result => {
this.messageList = result.map(item => {
return {
...item,
areaLabel: item.Outgoing__c ? `${item.AgentName__c} said at ${item.CreatedDate}` : `${item.CustomerName__c} said at ${item.CreatedDate}`
}
});
this.chatEnabled = true;
})
.catch(error => {
console.error('Error:', error);
this.errors = error;
})
.finally(()=>{
this.chatEnabled = true;
console.log('turing off the spinner');
const chatArea = this.template.querySelector('.messageContent');
if(chatArea){
chatArea.scrollTop = chatArea.scrollHeight
}
this.isSpinner = false;
this.setUpChatMessage();
})
}
setUpChatMessage(){
let chatInput = this.template.querySelector(".chat-input");
if(chatInput){
chatInput.addEventListener("keydown", (event) => {
console.log(`Enent handler added`)
if (event.key === "Enter") {
this.handleSendMessage();
}
});
}
}
handleSendMessage(){
console.log('Enter Clicked ', this.chatMessage );
if(!this.handleValidate()){
return;
}
this.isSpinner = true;
console.log(this.customerNo);
sendTextMessage({
messageContent : this.chatMessage,
toPhone : this.customerNo
})
.then(result => {
console.log(' message list ', result );
this.chatMessage = '';
this.messageList = [...this.messageList, result];
console.log(' Updated Message from ', this.messageList );
})
.catch(error => {
// TODO Error handling
})
.finally(()=>{
console.log('turing off the spinner');
this.isSpinner = false;
})
}
handlePhoneChange(event){
this.customerNo = event.target.value;
}
handleAnOtherChat(event){
event.preventDefault();
this.messageList = undefined;
this.chatEnabled = false;
}
handleValidate(){
const allValid = [
...this.template.querySelectorAll('lightning-input'),
].reduce((validSoFar, inputCmp) => {
inputCmp.reportValidity();
return validSoFar && inputCmp.checkValidity();
}, true);
return allValid;
}
registerErrorListener() {
// Invoke onError empApi method
onError((error) => {
console.log('Received error from server: ', JSON.stringify(error));
// Error contains the server-side error
});
}
handleSubscribe() {
// Callback invoked whenever a new event message is received
const messageCallback = this.handleEventResponse.bind(this);
// Invoke subscribe method of empApi. Pass reference to messageCallback
subscribe(this.channelName, -1, messageCallback).then((response) => {
this.subscription = response;
});
}
handleEventResponse(response) {
console.log('New message received: ', JSON.stringify(response));
let payload = response.data.payload;
let MessageId = payload.MessageId__c;
let CustomerPhone = payload.CustomerPhone__c;
if(this.customerNo === CustomerPhone){
getSingleMessage({
recordId : MessageId,
customerPhone : CustomerPhone
})
.then(result => {
this.messageList = [...this.messageList, result];
const chatArea = this.template.querySelector('.messageContent');
if(chatArea){
chatArea.scrollTop = chatArea.scrollHeight
}
})
.catch(error => {
// TODO Error handling
console.error(error);
});
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>55.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>WhatsApp Chat Component</masterLabel>
<targets>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
<target>lightning__UtilityBar</target>
<target>lightning__FlowScreen</target>
</targets>
</LightningComponentBundle>
public class WhatsAppLWCService {
@AuraEnabled//(cacheable=true)
public static List<WAMessage__c> listAllMessages(String customerPhone){
List<WAMessage__c> messages = new List<WAMessage__c>();
messages = [SELECT Id, Name, MessageContent__c, MessageType__c, CustomerName__c, AgentName__c, Outgoing__c, CreatedDate
FROM WAMessage__c
WHERE CustomerName__c =: customerPhone
Order By CreatedDate ASC
];
return messages;
}
@AuraEnabled
public static WAMessage__c getSingleMessage(String recordId, String customerPhone){
return [SELECT Id, Name, MessageContent__c, MessageType__c, CustomerName__c, AgentName__c, Outgoing__c, CreatedDate
FROM WAMessage__c
WHERE Id =: recordId
AND CustomerPhone__c =: customerPhone
Order By CreatedDate ASC
];
}
@AuraEnabled
public static WAMessage__c sendTextMessage(String messageContent, String toPhone){
WAMessage__c message = WhatsAppUtils.sendTextMessage(messageContent, toPhone);
return [SELECT Id, Name, MessageContent__c, MessageType__c, CustomerName__c, AgentName__c, Outgoing__c, CreatedDate
FROM WAMessage__c
WHERE Id =: message.Id
Order By CreatedDate ASC
];
}
@AuraEnabled(cacheable=true)
public static List<WAMessage__c> listAllMessageByCustomer(String customerPhone){
List<WAMessage__c> messages = new List<WAMessage__c>();
messages = [SELECT Id, Name, MessageContent__c, MessageType__c, CustomerName__c, AgentName__c, Outgoing__c, CreatedDate
FROM WAMessage__c
WHERE CustomerPhone__c =: customerPhone
Order By CreatedDate ASC
];
return messages;
}
}
public class WhatsAppMessage {
public entry[] entry;
public class entry {
public String id;
public changes[] changes;
}
public class changes {
public value value;
public String field;
}
public class value {
public String messaging_product;
public metadata metadata;
public contacts[] contacts;
public messages[] messages;
}
public class metadata {
public String display_phone_number;
public String phone_number_id;
}
public class contacts {
public profile profile;
public String wa_id;
}
public class profile {
public String name;
}
public class messages {
public context context;
public String fromx;
public String id;
public String timestamp;
public text text;
public String typex;
public reaction reaction;
public image image;
public image video;
}
public class context {
public String fromx;
public String id;
}
public class text {
public String body;
}
public class reaction{
public String emoji;
public String message_id;
}
public class image{
public String mime_type;
public String id;
public String sha256;
}
}
public class WhatsAppUtils {
public static List<WAMessage__c> listAllMessageByCustomer(String customerPhone){
List<WAMessage__c> messages = new List<WAMessage__c>();
messages = [SELECT Id, Name, MessageContent__c, MessageType__c, Outgoing__c FROM WAMessage__c WHERE CustomerPhone__c =: customerPhone];
return messages;
}
public static WAMessage__c sendTextMessage(String messageContent, String toPhone){
HttpRequest httpReq = new HttpRequest();
httpReq.setEndpoint('https://graph.facebook.com/v13.0/YOUR_ACCOUNT_ID/messages');
httpReq.setMethod('POST');
httpReq.setHeader('Content-Type', 'application/json');
httpReq.setHeader('Authorization', 'Bearer '+System.Label.WHATSAPPACCESSTOKEN);
String messageBody = '{'+
' "messaging_product": "whatsapp",'+
' "recipient_type": "individual",'+
' "to": "'+toPhone+'",'+
' "type": "text",'+
' "text": {'+
' "preview_url": false,'+
' "body": "'+messageContent+'"'+
' }'+
'}';
httpReq.setBody(messageBody);
Http http = new Http();
WAMessage__c salesforceMessage = new WAMessage__c();
try{
HttpResponse response = http.send(httpReq);
if( response.getStatusCode() == 200 ){
// Parse & Create Message Record
System.debug('Successful!');
WhatsAppUtils responseFromWA = (WhatsAppUtils) JSON.deserialize( response.getBody() , WhatsAppUtils.class);
salesforceMessage.MessageContent__c = messageContent;
salesforceMessage.CustomerPhone__c = toPhone;
salesforceMessage.MessageID__c = responseFromWA.messages.get(0).id;
salesforceMessage.MessageType__c = 'text';
salesforceMessage.Outgoing__c = True;
salesforceMessage.AgentName__c = UserInfo.getFirstName()+' '+ UserInfo.getLastName();
upsert salesforceMessage MessageID__c;
}
}catch(System.CalloutException ex){
System.debug(' CalloutException Executed '+ ex.getStackTraceString() );
System.debug(' CalloutException Executed '+ ex.getMessage() );
}catch(System.Exception ex){
System.debug(' System.Exception Executed '+ ex.getStackTraceString() );
}
return salesforceMessage;
}
public String messaging_product;
public contacts[] contacts;
public messages[] messages;
public class contacts {
public String input;
public String wa_id;
}
public class messages {
public String id;
}
}
@RestResource(urlMapping='/whatsapp/webhooks/v1/*')
global without sharing class WhatsAppWebhook {
private static Final String SIGNATURE_VALID_MESSAGE = 'Signature Verified';
private static Final String SIGNATURE_NOT_VALID_MESSAGE = 'Signature could not be verified';
@HttpGet // GET
global static void doGet() {
RestResponse response = RestContext.response;
RestRequest request = RestContext.request;
if(request.params.get('hub.verify_token') == 'WHATSAPPTOKEN'){
response.responseBody = Blob.valueOf( request.params.get('hub.challenge') );
}
}
@HttpPost // POST
global static void doPost() {
RestResponse response = RestContext.response;
response.addHeader('Content-type','application/json');
String responseString = RestContext.request.requestBody.toString();
Map<String, String> headers = RestContext.request.headers;
String responseValid = validateWhatsAppSignature(RestContext.request, responseString);
if(responseValid == SIGNATURE_VALID_MESSAGE){
System.debug(System.LoggingLevel.DEBUG, ' Headers Response From WhatsApp \n '+ JSON.serialize(headers) );
System.debug(System.LoggingLevel.DEBUG, ' Response From WhatsApp \n '+ responseString);
String finalResponseString = responseString.replace('type', 'typex');
WhatsAppMessage parentMessage = (WhatsAppMessage)JSON.deserialize( finalResponseString, WhatsAppMessage.class);
List<WhatsAppMessage.entry> messageEntries = parentMessage.entry;
if(messageEntries != null && messageEntries.size() > 0){
WhatsAppMessage.entry entryMessage = messageEntries.get(0);
List<WhatsAppMessage.changes> changeMessages = entryMessage.changes;
if(changeMessages != null && changeMessages.size() > 0){
WhatsAppMessage.changes changeMessage = changeMessages.get(0);
List<WhatsAppMessage.contacts> contactList = changeMessage.value.contacts;
List<WhatsAppMessage.messages> messageList = changeMessage.value.messages;
WhatsAppMessage.metadata metadata = changeMessage.value.metadata;
/* Create record into Salesforce */
WAMessage__c salesforceMessage = new WAMessage__c();
salesforceMessage.BusinessPhoneNumber__c = metadata != null ? metadata.display_phone_number : null;
if(contactList != null && contactList.size() > 0){
WhatsAppMessage.contacts contact = contactList.get(0);
salesforceMessage.CustomerPhone__c = contact.wa_id;
salesforceMessage.CustomerName__c = contact.profile.name;
}
if(messageList != null && messageList.size() > 0){
/* Simple Message */
WhatsAppMessage.messages message = messageList.get(0);
salesforceMessage.MessageID__c = message.id;
salesforceMessage.MessageType__c = message.typex;
salesforceMessage.MessageSentTime__c = System.now();
salesforceMessage.MessageContent__c = message.text != null? message.text.body : null;
/* If message is reaction */
salesforceMessage.Reaction__c = message.reaction != null ? message.reaction.emoji : null;
salesforceMessage.ParentMessageID__c = message.reaction != null ? message.reaction.message_id : null;
/* If message is Image */
salesforceMessage.ImageID__c = message.image != null ? message.image.id : null;
salesforceMessage.ImageType__c = message.image != null ? message.image.mime_type : null;
salesforceMessage.ImageSHA256__c = message.image != null ? message.image.sha256 : null;
/* If message is Video */
salesforceMessage.VideoId__c = message.video != null ? message.video.id : null;
salesforceMessage.VideoType__c = message.video != null ? message.video.mime_type : null;
salesforceMessage.VideoSHA256__c = message.video != null ? message.video.sha256 : null;
/* If the message is reply to another message */
salesforceMessage.ParentMessageID__c = message.context != null ? message.context.id : null;
upsert salesforceMessage MessageID__c;
/* Publish the Platform Event to be listened by LWC */
WAMessageEvent__e platformEvent = new WAMessageEvent__e();
platformEvent.MessageId__c = salesforceMessage.Id;
platformEvent.CustomerPhone__c = salesforceMessage.CustomerPhone__c;
Eventbus.publish( platformEvent );
}
}
}
}else{
response.responseBody = Blob.valueOf('{success:false, event:"Unknown","message:"'+responseValid+'"}');
response.statusCode = 401;
return;
}
response.statusCode = 200;
response.responseBody = Blob.valueOf('{success:true, event:"success"}');
}
private static String validateWhatsAppSignature(RestRequest request, String responseString) {
// Validate Stripe signature Start
Map<String, String> headers = request.headers;
String whatsAppSignature = headers.get('X-Hub-Signature-256');
String whatsAppPayload = RestContext.request.requestBody.toString();
// Verify the signature using 'hmacSHA256'. I have the Webhook key stored in a Custom Label
String whatsAppSecret = System.Label.WHATSAPPSECRET; // Facebook Application Secret Key
Blob signedPayload = Crypto.generateMac('hmacSHA256', Blob.valueOf(whatsAppPayload), Blob.valueOf( whatsAppSecret ));
String encodedPayload = 'sha256='+EncodingUtil.convertToHex(signedPayload);
// Return status code based on whether signed payload matches or not
String response = (encodedPayload == whatsAppSignature)? SIGNATURE_VALID_MESSAGE : SIGNATURE_NOT_VALID_MESSAGE;
return response;
// Validate Stripe signature End
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment