Mobile (Android Webview)

First, choose the HTML file index-debug-standard.html and rename it to index-mobile.html. (This is optional or you may choose to leave it like that)

Open the HTML file and copy the below codes to your website index or entry HTML file. Paste the codes inside your <head> tag.

<html>
<head>
  <meta charset="UTF-8">
  <title>Enterprise Web Chat</title>
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1,user-scalable=no" />
  <link rel='stylesheet prefetch' href='https://fonts.googleapis.com/css?family=Open+Sans'>
  <link rel="stylesheet" href="css/indigo.css">
  <link rel="stylesheet" href="css/widget.css">
  <link rel="stylesheet" href="css/lightslider.css">
  <link rel="stylesheet" href="css/intlTelInput.css">
	<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1,user-scalable=no" />
	<style type="text/css">
		html {
			background: url(images/bg-login.svg) no-repeat;
			height: 100%;
			background-size: cover;
		}
	</style>
  <!-- <script src="https://maps.googleapis.com/maps/api/js?key=<KEY>&libraries=places"></script> -->
  <script src="https://www.google.com/recaptcha/api.js"></script>
  <!-- <script src="http://code.responsivevoice.org/responsivevoice.js"></script> -->
  <script src='js/jquery-2.1.3.min.js'></script>
  <script src='js/iframeResizer.contentWindow.js'></script>
  <script src='js/iframeResizer.js'></script>
  <script src='js/lightslider.js'></script>  
  <script src='js/lazyload.min.js'></script>  
  <script src="js/cryptojs/pbkdf2.js"></script>
  <script src="js/cryptojs/aes.js"></script>
  <script src="js/cipher/aes-util.js"></script>
  <script src="js/stp.js"></script>
  <script src="js/sjs.js"></script>
  <script src="js/chat.js"></script>
  <script src="js/fuse.js"></script>
  <script src="js/speech.js"></script>
  <script src="js/intlTelInput.js"></script>
  <script>
  $(document).ready(function() {
	    Chat.init({
	        header: 'Welcome to Our Chat',
	        login_sub_header: 'Please tell us about yourself',
	        connect_message: 'Do you have questions ? <br>Come chat with us, we are here to help you',
	        chat_sub_header: 'Our agent will serve you shortly',
			url: 'https://<host>:<port>',
			client_id: '<client_id>',
			client_secret: '<client_secret>',	
	        type_placeholder: 'Type message...',
			avatar: '<bot_image>',
			icon_avatar: '<icon_image>',
			agent_avatar: '<agent_image>',
	        enable_attachment: true,
	        enable_voice: true,
	        enable_speech: true,
	        enable_queue: true,
	        enable_history: true,
	        compatibility_mode: false,
	        queue_text: "⏰NOMOR URUT: ",
	        enable_campaign: false,
	        campaign_avatar: '<campaign_image>',
			campaign_title: '<campaign_title>',
	        campaign_text: 'Hello 👋, What do you think about our service ?',
	        campaign_timer: 5000,
	        campaign_menu: [{
	            "label": "<Campaign Label>",
	            "value": "<Campaign Payload>",
	            "icon": "<campaign_icon>"
	        }, {
	            "label": "<Campaign Label>",
	            "value": "<Campaign Payload>",
	            "icon": "<campaign_icon>"
	        }, {
	            "label": "<Campaign Label>",
	            "value": "<Campaign Payload>",
	            "icon": "<campaign_icon>"
	        }],
	        max_upload_message: "File size limit exceeded. Maximum filesize is [max_filesize]",
	        selection_topic_placeholder: "Please select a topic",
	        selection_topic_member: ["Product", "Service"],
	        selection_topic_non_member: ["Hai", "Hello"],
			member_mode: false,
	        selection_topic_placeholder: "Please select a topic",
	        enable_service_hour: false,
	        service_hour: [{
	                "days": "sunday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "monday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "tuesday",
	                "startHour": "00:00",
	                "endHour": "15:52",
	                "holiday": true
	            },
	            {
	                "days": "wednesday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "thursday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "friday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "saturday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            }
	        ],
	        channel_id_email: "25b7fe4f7bd81aa0533be4963972ac74", /* channel id email */
	        subject_email: "Of service hour ticket",
	        send_email_success_message: "Email sent successfully !<br/><br/>Thanks, we have received your question or your complaint",
	        process_send_email_message: "Sending Email...",
	        process_send_email_error_message: "Email sent failed !",
			is_waiting_text_icon: true
	    });
	});
  </script>
</head>
<body>  
</body>
</html>

Remove all the HTML file but keep the index-mobile.html instead.

Copy the whole live chat directory to your designated path of your web hosting server.

Assume that you want to choose is the indigo theme, then navigate on the index-mobile.html and change the code using the color that you want.

<link rel="stylesheet" href="css/indigo.css">
  <link rel="stylesheet" href="css/widget.css">
  <link rel="stylesheet" href="css/lightslider.css">
  <link rel="stylesheet" href="css/intlTelInput.css">

Don’t forget to adjust the resource paths which are in index-mobile.html that are used to load files like js, CSS, or image, correspond to where the resources are kept in your server.

<html>
<head>
  <meta charset="UTF-8">
  <title>Enterprise Web Chat</title>
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1,user-scalable=no" />
  <link rel='stylesheet prefetch' href='https://fonts.googleapis.com/css?family=Open+Sans'>
  <link rel="stylesheet" href="css/indigo.css">
  <link rel="stylesheet" href="css/widget.css">
  <link rel="stylesheet" href="css/lightslider.css">
  <link rel="stylesheet" href="css/intlTelInput.css">
	<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1,user-scalable=no" />
	<style type="text/css">
		html {
			background: url(images/bg-login.svg) no-repeat;
			height: 100%;
			background-size: cover;
		}
	</style>
  <!-- <script src="https://maps.googleapis.com/maps/api/js?key=<KEY>&libraries=places"></script> -->
  <script src="https://www.google.com/recaptcha/api.js"></script>
  <!-- <script src="http://code.responsivevoice.org/responsivevoice.js"></script> -->
  <script src='js/jquery-2.1.3.min.js'></script>
  <script src='js/iframeResizer.contentWindow.js'></script>
  <script src='js/iframeResizer.js'></script>
  <script src='js/lightslider.js'></script>  
  <script src='js/lazyload.min.js'></script>  
  <script src="js/cryptojs/pbkdf2.js"></script>
  <script src="js/cryptojs/aes.js"></script>
  <script src="js/cipher/aes-util.js"></script>
  <script src="js/stp.js"></script>
  <script src="js/sjs.js"></script>
  <script src="js/chat.js"></script>
  <script src="js/fuse.js"></script>
  <script src="js/speech.js"></script>
  <script src="js/intlTelInput.js"></script>
  <script>
  $(document).ready(function() {
	    Chat.init({
	        header: 'Welcome to Our Chat',
	        login_sub_header: 'Please tell us about yourself',
	        connect_message: 'Do you have questions ? <br>Come chat with us, we are here to help you',
	        chat_sub_header: 'Our agent will serve you shortly',
			url: 'https://<host>:<port>',
			client_id: '<client_id>',
			client_secret: '<client_secret>',	
	        type_placeholder: 'Type message...',
			avatar: '<bot_image>',
			icon_avatar: '<icon_image>',
			agent_avatar: '<agent_image>',
	        enable_attachment: true,
	        enable_voice: true,
	        enable_speech: true,
	        enable_queue: true,
	        enable_history: true,
	        compatibility_mode: false,
	        queue_text: "⏰NOMOR URUT: ",
	        enable_campaign: false,
	        campaign_avatar: '<campaign_image>',
			campaign_title: '<campaign_title>',
	        campaign_text: 'Hello 👋, What do you think about our service ?',
	        campaign_timer: 5000,
	        campaign_menu: [{
	            "label": "<Campaign Label>",
	            "value": "<Campaign Payload>",
	            "icon": "<campaign_icon>"
	        }, {
	            "label": "<Campaign Label>",
	            "value": "<Campaign Payload>",
	            "icon": "<campaign_icon>"
	        }, {
	            "label": "<Campaign Label>",
	            "value": "<Campaign Payload>",
	            "icon": "<campaign_icon>"
	        }],
	        max_upload_message: "File size limit exceeded. Maximum filesize is [max_filesize]",
	        selection_topic_placeholder: "Please select a topic",
	        selection_topic_member: ["Product", "Service"],
	        selection_topic_non_member: ["Hai", "Hello"],
			member_mode: false,
	        selection_topic_placeholder: "Please select a topic",
	        enable_service_hour: false,
	        service_hour: [{
	                "days": "sunday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "monday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "tuesday",
	                "startHour": "00:00",
	                "endHour": "15:52",
	                "holiday": true
	            },
	            {
	                "days": "wednesday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "thursday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "friday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            },
	            {
	                "days": "saturday",
	                "startHour": "00:00",
	                "endHour": "23:59",
	                "holiday": true
	            }
	        ],
	        channel_id_email: "25b7fe4f7bd81aa0533be4963972ac74", /* channel id email */
	        subject_email: "Of service hour ticket",
	        send_email_success_message: "Email sent successfully !<br/><br/>Thanks, we have received your question or your complaint",
	        process_send_email_message: "Sending Email...",
	        process_send_email_error_message: "Email sent failed !",
			is_waiting_text_icon: true
	    });
	});
  </script>
</head>
<body>  
</body>
</html>

Lastly, adjust the url, client_id and client_secret properties to correspond your backend’s channel configuration. Please refer back to How to Connect Client to Live chat Channel for more information.

In that section we put our code in the existing website, while in this section we have to host the live chat independently.

Below is a sample code for configuring your webview which displays that already hosted index-mobile.html.

Configure Web View

You need to set enable Javascript and DomStorage For LocalStorage.

webview.getSettings().setJavaScriptEnabled(true);
webview.getSettings().setDatabaseEnabled(true);
webview.getSettings().setDomStorageEnabled(true);

Now you can load the URL.

webview.loadUrl("https://hostname/webchat/index-debug-standard.html");

To customize after page load finish, you may run this code.

webview.setWebViewClient(new WebViewClient() {
     @Override
     public void onPageFinished(WebView view, String url) {
          progressBar.setVisibility(View.GONE);
          super.onPageFinished(view, url);
     }
});

Enable Voice to Text (Speech Recognition)

To enable this feature, first, you need to enable permission for RECORD_AUDIO on android manifest.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.imi.dolphin.bella">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
		...
		...
</manifest>

Then, initialize Properties to enable Record Audio Permission.

private static final int RECORD_AUDIO_REQUEST_CODE = 123;
private PermissionRequest permissionRequest;

The permissionRequest property is used to accommodate requests from the web view that will be granted access later.

When the microphone icon is clicked, the browser will ask for permission to the device, your application must catch the request. can be done by:

private void loadLiveChat() {
		...
		...

    webview.setWebChromeClient(new WebChromeClient() {
        @Override
        public void onPermissionRequest(PermissionRequest request) {
            if (request != null) {
                permissionRequest = request;
                askPermission();
            }
        }
    });
}

private void askPermission() {
    if (ContextCompat.checkSelfPermission(getApplicationContext(), 
						Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
        
				ActivityCompat.requestPermissions(
						MainActivity.this, 
						new String[]{Manifest.permission.RECORD_AUDIO}, 
						RECORD_AUDIO_REQUEST_CODE
				);
    } else {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            permissionRequest.grant(permissionRequest.getResources());
        }
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if(requestCode == RECORD_AUDIO_REQUEST_CODE){
        if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                permissionRequest.grant(permissionRequest.getResources());
            }
            Toast.makeText(getApplicationContext(), "Permission Granted", Toast.LENGTH_SHORT).show();
        }
    }
}

Enable Attachment

You need to enable permission for READ_EXTERNAL_STORAGE on android manifest.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.imi.dolphin.bella">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.RECORD_AUDIO"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
		...
</manifest>

Then, initialize Properties to access file.

private static final int INPUT_FILE_REQUEST_CODE = 1;
private ValueCallback<Uri[]> mFilePathCallback;

Configure Web View to access file access.

private void loadLiveChat() {
		...
    webview.getSettings().setLoadWithOverviewMode(true);
    webview.getSettings().setAllowFileAccess(true);
		...
		...
}

When the attachment icon is clicked, the browser will ask to open device storage. Can be done by:

private void loadLiveChat() {
    ...
		...

    webview.setWebChromeClient(new WebChromeClient() {
        ...
				...
				@Override
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) { 
            if (mFilePathCallback != null) {
                mFilePathCallback.onReceiveValue(null);
            }
            mFilePathCallback = filePathCallback;

            Intent chooserIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
            startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE);
            return true;
        }
    });
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        if (requestCode != INPUT_FILE_REQUEST_CODE || mFilePathCallback == null) {
            super.onActivityResult(requestCode, resultCode, data);
            return;
        }
        Uri[] results = null;
        if (resultCode == Activity.RESULT_OK) {
            if (data != null) {
                String dataString = data.getDataString();
                results = new Uri[]{Uri.parse(dataString)};
            }
        }
        mFilePathCallback.onReceiveValue(results);
        mFilePathCallback = null;
    }
}

Below is an example of the final code if you are already following the step above.

package com.imi.dolphin.bella;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.webkit.PermissionRequest;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

public class MainActivity extends AppCompatActivity {

    /**
     * Properties to access file
     */
    private static final int INPUT_FILE_REQUEST_CODE = 1;
    private ValueCallback<Uri[]> mFilePathCallback;

    /**
     * Properties to enable Record Audio Permission
     * <p>
     * the permissionRequest property is used to accommodate requests from the web view that will be granted access later
     */
    private static final int RECORD_AUDIO_REQUEST_CODE = 123;
    private PermissionRequest permissionRequest;

    /**
     * Initial Component Web View and Progress Bar
     */
    private WebView webview;
    private ProgressBar progressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        init();
        loadLiveChat();
    }

    /**
     * Configure Web View
     */
    @SuppressLint("SetJavaScriptEnabled")
    private void loadLiveChat() {
        webview.loadUrl("<Server URL>");

        // Configure WebView To set Enable Javascript True And To Activate DomStorage True For LocalStorage
        webview.getSettings().setJavaScriptEnabled(true);
        webview.getSettings().setDatabaseEnabled(true);
        webview.getSettings().setDomStorageEnabled(true);

        // Configure WebView to access file access
        webview.getSettings().setLoadWithOverviewMode(true);
        webview.getSettings().setAllowFileAccess(true);

        webview.setWebViewClient(new WebViewClient() {
            @Override
            public void onPageFinished(WebView view, String url) {
                progressBar.setVisibility(View.GONE);
                super.onPageFinished(view, url);
            }
        });
        webview.setWebChromeClient(new WebChromeClient() {
            @Override
            public void onPermissionRequest(PermissionRequest request) {
                if (request != null) {
                    permissionRequest = request;
                    askPermission();
                }
            }

            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
                // Double check that we don't have any existing callbacks
                if (mFilePathCallback != null) {
                    mFilePathCallback.onReceiveValue(null);
                }
                mFilePathCallback = filePathCallback;

                Intent chooserIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
                startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE);
                return true;
            }
        });
    }

    /**
     * Binding Item
     */
    private void init() {
        webview = findViewById(R.id.web_view);
        progressBar = findViewById(R.id.progress_bar);
        progressBar.setVisibility(View.VISIBLE);
    }

    /**
     * ask permission for Record Audio
     */
    private void askPermission() {
        if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.RECORD_AUDIO}, RECORD_AUDIO_REQUEST_CODE);
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                permissionRequest.grant(permissionRequest.getResources());
            }
        }
    }

    /**
     * Catch permission from user
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == RECORD_AUDIO_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    permissionRequest.grant(permissionRequest.getResources());
                }
                Toast.makeText(getApplicationContext(), "Permission Granted", Toast.LENGTH_SHORT).show();
            }
        }
    }

    /**
     * Catch File data from user
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (requestCode != INPUT_FILE_REQUEST_CODE || mFilePathCallback == null) {
                super.onActivityResult(requestCode, resultCode, data);
                return;
            }
            Uri[] results = null;