Mobile (IOS 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

To enable JavaScript and configure the web view, you need to set the necessary properties.

let webConfiguration = WKWebViewConfiguration()
webConfiguration.defaultWebpagePreferences.allowsContentJavaScript = true
webConfiguration.websiteDataStore = WKWebsiteDataStore.default()

let webView = WKWebView(frame: .zero, configuration: webConfiguration)

Now you can load the URL.

webView.load(URLRequest(url: URL(string: "https://hostname/webchat/indexdebug-standard.html")!))

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

func webView(
    _ webView: WKWebView,
    didStartProvisionalNavigation navigation: WKNavigation!
) {
    DispatchQueue.main.async {
        self.parent.progress = 0.3
        self.parent.isLoading = true
    }
}

func webView(
    _ webView: WKWebView,
    didFinish navigation: WKNavigation!
) {
    DispatchQueue.main.async {
        self.parent.progress = 1.0
        self.parent.isLoading = false
    }
}

Enable Voice to Text (Speech Recognition)

To enable this feature, the WebView needs to request permission for recording audio.

func webView(
    _ webView: WKWebView,
    requestMediaCapturePermissionFor origin: WKSecurityOrigin,
    initiatedByFrame frame: WKFrameInfo,
    type: WKMediaCaptureType,
    decisionHandler: @escaping WKPermissionDecision
) {
    if #available(iOS 17.0, *) {
        AVAudioApplication.requestRecordPermission { granted in
            DispatchQueue.main.async {
                decisionHandler(granted ? .grant : .deny)
            }
        }
    } else {
        AVAudioSession.sharedInstance().requestRecordPermission { granted in
            DispatchQueue.main.async {
                decisionHandler(granted ? .grant : .deny)
            }
        }
    }
}

Enable Attachment

To enable file attachments, implement an image picker in the WebView delegate.

func webView(
    _ webView: WKWebView,
    runJavaScriptConfirmPanelWithMessage message: String,
    initiatedByFrame frame: WKFrameInfo,
    completionHandler: @escaping (Bool) -> Void
) {
    let picker = UIImagePickerController()
    picker.delegate = self
    picker.sourceType = .photoLibrary
    
    if let windowScene = UIApplication.shared.connectedScenes
        .compactMap({ $0 as? UIWindowScene })
        .first,
       let rootViewController = windowScene.windows.first?.rootViewController {
        rootViewController.present(picker, animated: true)
    }
}

Once an image is selected, the selected file URL must be handled.

func imagePickerController(
    _ picker: UIImagePickerController,
    didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
    picker.dismiss(animated: true, completion: nil)
    
    if let imageURL = info[.imageURL] as? URL {
        filePathCallback?([imageURL])
    } else {
        filePathCallback?(nil)
    }
    
    filePathCallback = nil
}

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    picker.dismiss(animated: true, completion: nil)
    filePathCallback?(nil)
    filePathCallback = nil
}

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

import SwiftUI
@preconcurrency import WebKit
import AVFoundation

struct WebView: UIViewRepresentable {
    @Binding var progress: Double
    @Binding var isLoading: Bool

    class Coordinator: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        var parent: WebView
        private let audioSession = AVAudioSession.sharedInstance()
        private var filePathCallback: (([URL]?) -> Void)?

        init(_ parent: WebView) {
            self.parent = parent
        }

        func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
            DispatchQueue.main.async {
                self.parent.progress = 0.3
                self.parent.isLoading = true
            }
        }

        func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
            DispatchQueue.main.async {
                self.parent.progress = 1.0
                self.parent.isLoading = false
            }
        }

        func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
            // Handle messages from JavaScript
        }

        func webView(
            _ webView: WKWebView,
            requestMediaCapturePermissionFor origin: WKSecurityOrigin,
            initiatedByFrame frame: WKFrameInfo,
            type: WKMediaCaptureType,
            decisionHandler: @escaping (WKPermissionDecision) -> Void
        ) {
            if #available(iOS 17.0, *) {
                AVAudioApplication.requestRecordPermission { granted in
                    DispatchQueue.main.async {
                        decisionHandler(granted ? .grant : .deny)
                    }
                }
            } else {
                audioSession.requestRecordPermission { granted in
                    DispatchQueue.main.async {
                        decisionHandler(granted ? .grant : .deny)
                    }
                }
            }
        }

        func webView(
            _ webView: WKWebView,
            runJavaScriptConfirmPanelWithMessage message: String,
            initiatedByFrame frame: WKFrameInfo,
            completionHandler: @escaping (Bool) -> Void
        ) {
            let picker = UIImagePickerController()
            picker.delegate = self
            picker.sourceType = .photoLibrary

            if let windowScene = UIApplication.shared.connectedScenes
                .compactMap({ $0 as? UIWindowScene })
                .first,
               let rootViewController = windowScene.windows.first?.rootViewController {
                rootViewController.present(picker, animated: true)
            }
        }

        func imagePickerController(
            _ picker: UIImagePickerController,
            didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
        ) {
            picker.dismiss(animated: true, completion: nil)

            if let imageURL = info[.imageURL] as? URL {
                filePathCallback?([imageURL])
            } else {
                filePathCallback?(nil)
            }
            filePathCallback = nil
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            picker.dismiss(animated: true, completion: nil)
            filePathCallback?(nil)
            filePathCallback = nil
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> WKWebView {
        let webConfiguration = WKWebViewConfiguration()
        webConfiguration.defaultWebpagePreferences.allowsContentJavaScript = true
        webConfiguration.websiteDataStore = WKWebsiteDataStore.default()

        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.uiDelegate = context.coordinator
        webView.navigationDelegate = context.coordinator
        webView.load(URLRequest(url: URL(string: "Server URL")!))

        return webView
    }

    func updateUIView(_ uiView: WKWebView, context: Context) {}
}

struct ContentView: View {
    @State private var progress: Double = 0.0
    @State private var isLoading: Bool = true

    var body: some View {
        ZStack {
            WebView(progress: $progress, isLoading: $isLoading)

            if isLoading {
                ProgressView(value: progress)
                    .progressViewStyle(LinearProgressViewStyle())
                    .frame(width: UIScreen.main.bounds.width * 0.4, height: 6)
                    .background(Color.gray.opacity(0.3))
                    .cornerRadius(3)
            }
        }
        .edgesIgnoringSafeArea(.all)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

If you want to try our code, please click this to get started.

Last updated

Was this helpful?