Jetlink Mobil SDK Webview Entegrasyon Kılavuzu

Bu doküman, Jetlink Mobil SDK'sının native iOS ve Android uygulamalarına entegrasyonu sırasında yapılması gereken yapılandırmaları açıklamaktadır.

1. Genel Bakış

Jetlink Mobil SDK, native uygulama içerisinde WKWebView (iOS) veya WebView (Android) bileşeni üzerinden çalışan bir web arayüzü olarak yüklenmektedir. Sohbet arayüzü, sesli görüşme, dosya yükleme ve mikrofon erişimi gibi özellikler bu webview içerisinde çalıştığından, native tarafta belirli yapılandırmaların doğru biçimde tamamlanması zorunludur. Söz konusu yapılandırmalar tamamlanmadığı takdirde, ilgili özellikler son kullanıcıya sunulamayacak veya beklenen biçimde çalışmayacaktır.

Bu dokümanda; webview üzerinden native tarafa iletilen olaylar, native tarafta yapılması gereken izin ve davranış yapılandırmaları ile sıkça karşılaşılan entegrasyon sorunlarına yönelik çözümler yer almaktadır.

1. Uygulamanıza Bir Chatbot İkonu Ekleyin

Uygulamanızda uygun bir chatbot ikonu kullanın. Bu ikon; menüde, üst bilgi (header) alanında veya kullanıcıların chatbot arayüzüne kolayca erişebileceği herhangi bir noktada konumlandırılabilir.

2. Chatbot İkonuna Tıklandığında Jetlink WebView URL'ini Açın

Jetlink WebView URL bilgisine Jetlink Dashboard üzerinden ulaşabilirsiniz:

Settings → iOS

Jetlink WebView URL formatı aşağıdaki gibidir:

https://public.jetlink.io/Home/MobilSDK?appId=<YOUR-APP-ID>&appKey=<YOUR-APP-KEY>
&mobileSDKType=ios&mobileDeviceName=<DEVICE-NAME>&mobileDeviceOS=<DEVICE-OS-VERSION>


Kullanıcı Giriş Bilgileri ile Jetlink UI'ı Başlatma

Jetlink WebView URL'ini açarken kullanıcıya ait bilgileri de iletebilirsiniz. Örneğin:

  • Ad
  • Soyad
  • E-posta adresi
  • Telefon numarası
  • Sisteminizdeki benzersiz kullanıcı ID'si

Örnek URL:

https://public.jetlink.io/Home/MobilSDK?appId=<YOUR-APP-ID>&appKey=<YOUR-APP-KEY>
&mobileSDKType=ios&mobileDeviceName=<DEVICE-NAME>&mobileDeviceOS=<DEVICE-OS-VERSION>
&username=<USER-FIRST-NAME>&userSurname=<USER-SURNAME>
&userEmail=<USER-EMAIL>&userPhone=<USER-PHONE-NUMBER>
&userSourceUserId=<USER-UNIQUE-ID-IN-YOUR-SYSTEM>

Kullanıcı Bilgilerinin Aktarılması

Jetlink WebView URL'i üzerinden sisteminizde bulunan kullanıcı bilgilerini iletebilirsiniz. Örneğin:

  • Ad
  • Soyad
  • E-posta
  • Telefon numarası
  • Benzersiz kullanıcı ID'si

Bu bilgilerin tamamını veya yalnızca ihtiyaç duyduğunuz kısmını gönderebilirsiniz. Jetlink arayüzü bu bilgilerle açılır ve kullanıcı daha sonraki görüşmelerinde aynı bilgiler üzerinden tanınarak sohbet geçmişiyle ilişkilendirilebilir.

Hepsi bu kadar. Kullanıcılarınız artık mobil uygulamanız içerisinden Jetlink chatbot ile mesajlaşmaya başlayabilirler. 🚀

2. Web Tarafından Native Tarafa İletilen Olaylar

Webview içerisinde çalışan SDK, belirli durumlarda native uygulamaya window.postMessage arayüzü üzerinden olaylar iletmektedir. Native uygulamanın bu olayları dinleyerek uygun aksiyonları gerçekleştirmesi gerekmektedir.

2.1. OPEN_APP_SETTINGS Olayı

Tetiklenme Koşulu: Kullanıcı sesli görüşme başlatmak istediğinde, mikrofon erişimi daha önce reddedilmişse SDK, "Ayarlara Git" ve "Vazgeç" seçeneklerini içeren bir uyarı görüntüler. Kullanıcı "Ayarlara Git" seçeneğini tercih ettiğinde bu olay native tarafa iletilir. Bu olay, mikrofon izni reddedilmiş olduğu sürece kullanıcının sesli görüşme ekranına her girişinde yeniden tetiklenebilmektedir. Native tarafın bu olayı aynı oturum içerisinde birden fazla kez alacak biçimde yapılandırılmış olması gerekmektedir.

Olay Yapısı:

{

  "type": "OPEN_APP_SETTINGS",

  "source": "jetlink-bot"

}

Beklenen Native Aksiyon: Uygulamanın izin yönetimi ekranının cihaz sistem ayarlarında açılması.

iOS Tarafı (WKWebView)

WKScriptMessageHandler arayüzü üzerinden olayın dinlenmesi gerekmektedir.

let userContentController = WKUserContentController()

userContentController.add(self, name: "OPEN_APP_SETTINGS")


let config = WKWebViewConfiguration()

config.userContentController = userContentController


func userContentController(_ userContentController: WKUserContentController,

                           didReceive message: WKScriptMessage) {

    if message.name == "OPEN_APP_SETTINGS" {

        if let url = URL(string: UIApplication.openSettingsURLString) {

            UIApplication.shared.open(url)

        }

    }

}

Handler kaydı (userContentController.add), WKWebView instance'ının her oluşturulmasında yapılandırma aşamasında gerçekleştirilmelidir. WebView yeniden oluşturulduğunda handler'ın yeniden kaydedilmemesi durumunda olay native tarafa iletilemez.

webView.uiDelegate = self atamasının yapılmış olması gerekmektedir.

Android Tarafı (WebView)

JavascriptInterface üzerinden bir köprü tanımlanması gerekmektedir.

class JetlinkAndroidBridge(private val activity: Activity) {

    @JavascriptInterface

    fun openAppSettings() {

        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {

            data = Uri.fromParts("package", activity.packageName, null)

        }

        activity.startActivity(intent)

    }

}


webView.addJavascriptInterface(JetlinkAndroidBridge(this), "JetlinkAndroidBridge")

SDK, window.JetlinkAndroidBridge.openAppSettings() ve window.postMessage kanallarını birbirinden bağımsız olarak eş zamanlı biçimde çalıştırmaktadır. Her iki kanalın da native tarafta desteklenmesi tavsiye edilmektedir.

3. Mikrofon Erişimi Yapılandırması

3.1. iOS


Info.plist dosyasına aşağıdaki anahtarın eklenmesi zorunludur.

<key>NSMicrophoneUsageDescription</key>

<string>Sesli sohbet özelliğini kullanabilmek için mikrofon erişimi gereklidir.</string>

iOS 15 ve üzeri sürümlerde, WKWebView üzerinden gelen medya erişim taleplerinin onaylanabilmesi için WKUIDelegate üzerinde aşağıdaki metodun uygulanması gerekmektedir.

extension ViewController: WKUIDelegate {

    func webView(_ webView: WKWebView,

                 requestMediaCapturePermissionFor origin: WKSecurityOrigin,

                 initiatedByFrame frame: WKFrameInfo,

                 type: WKMediaCaptureType,

                 decisionHandler: @escaping (WKPermissionDecision) -> Void) {

        decisionHandler(.grant)

    }

}

webView.uiDelegate = self atamasının yapılmış olması gerekmektedir.

3.2. Android

AndroidManifest.xml dosyasına aşağıdaki izinlerin eklenmesi gerekmektedir.

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

<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

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

WebView içerisinden gelen getUserMedia taleplerinin karşılanabilmesi için WebChromeClient.onPermissionRequest metodunun aşağıdaki biçimde override edilmesi gerekmektedir.

webView.webChromeClient = object : WebChromeClient() {

    override fun onPermissionRequest(request: PermissionRequest) {

        activity.runOnUiThread {

            request.grant(request.resources)

        }

    }

}


Android 6.0 ve üzeri sürümlerde runtime izin akışının native tarafta da yürütülmesi gerekmektedir.

ActivityCompat.requestPermissions(

    activity,

    arrayOf(Manifest.permission.RECORD_AUDIO),

    REQUEST_CODE_AUDIO

)

4. Mikrofon İzin Akışı ve Permission State Senkronizasyonu

Sesli görüşme özelliğinin WebView içerisinde tutarlı çalışabilmesi için native uygulamanın app-level mikrofon izin durumunu WebView medya izin akışına doğru şekilde yansıtması gerekmektedir. Web tarafı getUserMedia({ audio: true }) çağrısı yapar; bu çağrının gerçek grant/deny kararı Android ve iOS native WebView katmanında verilmelidir.

4.1. Beklenen Kullanıcı Akışı

  • Kullanıcı sesli konuşma ikonuna tıkladığımda mikrofon izin kontrolü yapılmalıdır.

  • Kullanıcı daha önce mikrofon izni vermediyse native mikrofon izin popup gösterilmelidir.

  • Kullanıcı native popup üzerinde izin verirse kullanıcı doğrudan sesli konuşma ekranına yönlendirilmelidir.

  • Kullanıcı native popup üzerinde izin vermezse aynı kullanıcı aksiyonu içerisinde veya hemen sonrasında custom popup gösterilmemelidir.

  • Native popup reddedildikte sonra kullanıcımevcutsayfada kalmalıdır.

  • Kullanıcı daha sonra yenidensesli konuşma ikonunatıklarsa nativepopupaçılabiliyorsaaçılmalı,açılamıyorsacustom permission popupgösterilmelidir.

  • Custom popup üzerinden kullanıcı"Ayarlara Git" aksiyonu ile cihaz ayarlarına yönlendirilmelidir.

  • Kullanıcı daha önce mikrofon iznini vermişse herhangi bir popup tekrar gösterilmeden kullanıcı doğrudansesli konuşma ekranına yönlendirilmelidir.

  • Aynı kullanıcı aksiyonu içerisinde üst üste birden fazla permission popup gösterilmemelidir.

4.2. Native Tarafından Web'e Permission State Bildirimi

WebView yeniden oluşturulduğunda, navigation tamamlandığında veya kullanıcı cihaz ayarlarından uygulamaya geri döndüğünde native taraf güncel mikrofon izin durumunu WebView'e bildirmelidir. Bu bildirim web tarafındaki custom popup kararını günceller.

Desteklenen event isimleri aşağıdaki gibidir:

  • micPermissionStateChanged: Genel state eventidir. detail.state değeri granted, denied veya prompt olmalıdır.

  • micPermissionGranted: App-level mikrofon izni verilmiştir.

  • micPermissionDenied: App-level mikrofon izni reddedilmiş veya cihaz ayarlarındankapatılmıştır.

  • micPermissionUndetermined:Kullanıcı henüz native izin kararı vermemiştir; native prompt gösterilebilir.

window.dispatchEvent(new CustomEvent("micPermissionStateChanged", {

    detail: { state: "granted" } // granted | denied | prompt

}));


window.dispatchEvent(new Event("micPermissionGranted"));

window.dispatchEvent(new Event("micPermissionDenied"));

window.dispatchEvent(new Event("micPermissionUndetermined"));

Alternatif olarak window.postMessage payload'i de kullanılabilir.

window.postMessage({

    type: "micPermissionStateChanged",

    state: "granted" // granted | denied | prompt

}, "*");


Bu bildirim custom popup state'ini günceller. Ancak gerçek medya izni, getUserMedia çağrısı sırasında Android'de WebChromeClient.onPermissionRequest, iOS'ta WKUIDelegate.requestMediaCapturePermissionFor tarafından app-level permission state'e göre grant/deny edilmelidir.

4.3. Android Settings Dönüşü

Kullanıcı custom popup üzerinden uygulama ayarlarına yönlendirildikten sonra geri döndüğünde Android tarafında onResume içinde RECORD_AUDIO izni tekrar okunmalı ve WebView'e state bildirilmelidir.

override fun onResume() {

    super.onResume()

    notifyMicPermissionStateToWebView()

}


private fun notifyMicPermissionStateToWebView() {

    val state = if (

        ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==

        PackageManager.PERMISSION_GRANTED

    ) {

        "granted"

    } else {

        "denied"

    }


    val eventName = when (state) {

        "granted" -> "micPermissionGranted"

        "denied" -> "micPermissionDenied"

        else -> "micPermissionUndetermined"

    }


    val script = """

        window.dispatchEvent(new CustomEvent('micPermissionStateChanged', {

            detail: { state: '$state' }

        }));

        window.dispatchEvent(new Event('$eventName'));

    """.trimIndent()


    webView.post {

        webView.evaluateJavascript(script, null)

    }

}

4.4. iOS Settings Dönüşü

Kullanıcı cihaz ayarlarından geri döndüğünde iOS tarafında app-level mikrofon izni tekrar okunmalı; WebView yeniden oluşturulduysa navigation tamamlandıktan sonra, aktif WebView varsa doğrudan JavaScript event dispatch edilmelidir.

func notifyMicPermissionStateToWebView() {

    let state: String

    let eventName: String


    switch AVAudioSession.sharedInstance().recordPermission {

    case .granted:

        state = "granted"

        eventName = "micPermissionGranted"

    case .denied:

        state = "denied"

        eventName = "micPermissionDenied"

    case .undetermined:

        state = "prompt"

        eventName = "micPermissionUndetermined"

    @unknown default:

        state = "denied"

        eventName = "micPermissionDenied"

    }


    let script = """

    window.dispatchEvent(new CustomEvent('micPermissionStateChanged', {

        detail: { state: '\(state)' }

    }));

    window.dispatchEvent(new Event('\(eventName)'));

    """


    webView.evaluateJavaScript(script, completionHandler: nil)

}

4.5. iOS Media Capture Kararı

iOS 15 ve üzeri sürümlerde WKWebView medya erişim isteği geldiğinde native taraf app-level mikrofon izin durumunu okuyarak karar vermelidir. App-level mikrofon izni granted ise WebView yeniden oluşturulmuş olsa bile tekrar native izin popup'ı gösterilmeden .grant dönülmelidir.

@available(iOS 15.0, *)

func webView(_ webView: WKWebView,

             requestMediaCapturePermissionFor origin: WKSecurityOrigin,

             initiatedByFrame frame: WKFrameInfo,

             type: WKMediaCaptureType,

             decisionHandler: @escaping (WKPermissionDecision) -> Void) {

    guard type == .microphone || type == .cameraAndMicrophone else {

        decisionHandler(.deny)

        return

    }


    switch AVAudioSession.sharedInstance().recordPermission {

    case .granted:

        decisionHandler(.grant)

    case .denied:

        decisionHandler(.deny)

    case .undetermined:

        decisionHandler(.prompt)

    @unknown default:

        decisionHandler(.deny)

    }

}

5. Dosya Eki ve Dosya Seçici Yapılandırması

Sohbet arayüzündeki dosya ekleme (attachment) butonuna basıldığında native dosya seçicinin açılması beklenmektedir. Bu davranış webview içerisinde <input type="file"> etkileşimi ile yönetilmekte olup, native tarafın ilgili olayları desteklemesi gerekmektedir.

5.1. Android

WebChromeClient.onShowFileChooser metodu override edilmediği takdirde dosya seçici penceresi açılmamaktadır. Bu konu Android entegrasyonlarında en sık karşılaşılan eksikliktir.

private var fileChooserCallback: ValueCallback<Array<Uri>>? = null

private val FILE_CHOOSER_REQUEST_CODE = 1001


webView.webChromeClient = object : WebChromeClient() {

    override fun onShowFileChooser(

        webView: WebView,

        filePathCallback: ValueCallback<Array<Uri>>,

        fileChooserParams: FileChooserParams

    ): Boolean {

        fileChooserCallback?.onReceiveValue(null)

        fileChooserCallback = filePathCallback

        val intent = fileChooserParams.createIntent()

        try {

            startActivityForResult(intent, FILE_CHOOSER_REQUEST_CODE)

        } catch (e: ActivityNotFoundException) {

            fileChooserCallback = null

            return false

        }

        return true

    }


    override fun onPermissionRequest(request: PermissionRequest) {

        activity.runOnUiThread { request.grant(request.resources) }

    }

}

onActivityResult metodu üzerinden seçim sonucunun geri iletilmesi gerekmektedir.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

    super.onActivityResult(requestCode, resultCode, data)

    if (requestCode == FILE_CHOOSER_REQUEST_CODE) {

        val results = WebChromeClient.FileChooserParams.parseResult(resultCode, data)

        fileChooserCallback?.onReceiveValue(results)

        fileChooserCallback = null

    }

}

Galeri ve kamera erişimi için aşağıdaki izinlerin tanımlanması gerekmektedir.

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<uses-permission android:name="android.permission.CAMERA" />

5.2. iOS

WKWebView dosya seçici davranışını varsayılan olarak desteklemektedir. Yalnızca Info.plist dosyasında aşağıdaki kullanım açıklamalarının tanımlı olması gerekmektedir.

<key>NSPhotoLibraryUsageDescription</key>

<string>Sohbet üzerinden dosya gönderebilmek için fotoğraf erişimine izin verin.</string>

<key>NSCameraUsageDescription</key>

<string>Sohbet üzerinden fotoğraf çekebilmek için kameraya erişim gereklidir.</string>

6. Webview Yerleşimi

6.1. iOS WKWebView

webView.scrollView.contentInsetAdjustmentBehavior = .never

webView.scrollView.bounces = false

webView.scrollView.alwaysBounceVertical = false

webView.scrollView.alwaysBounceHorizontal = false

WebView constraint tanımlarının view.safeAreaLayoutGuide.bottomAnchor yerine view.bottomAnchor referansı ile yapılması gerekmektedir.

NSLayoutConstraint.activate([

    webView.topAnchor.constraint(equalTo: view.topAnchor),

    webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),

    webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),

    webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)

])

View controller seviyesinde aşağıdaki yapılandırmalar tanımlanmalıdır.

edgesForExtendedLayout = .all

extendedLayoutIncludesOpaqueBars = true

7. Sesli Görüşme - Arka Plan Davranışı

Kullanıcının uygulamayı arka plana alması veya cihaz ekranını kilitlemesi durumunda, aktif sesli görüşme SDK tarafından otomatik olarak sonlandırılmaktadır. Söz konusu davranış visibilitychange, pagehide ve blur olayları üzerinden yönetilmektedir. Native tarafta ek bir aksiyon alınmasına gerek bulunmamakla birlikte, iOS için ses oturumu yapılandırmasının aşağıdaki biçimde tanımlanması tavsiye edilmektedir.

try? AVAudioSession.sharedInstance().setCategory(

    .playAndRecord,

    mode: .voiceChat,

    options: [.defaultToSpeaker, .allowBluetooth]

)

try? AVAudioSession.sharedInstance().setActive(true)

8. Doğrulama Listesi

Entegrasyon tamamlandıktan sonra aşağıdaki senaryoların her birinin doğrulanması gerekmektedir.

  • Sohbet arayüzü açılmakta ve mesaj gönderilebilmektedir.

  • Sesli görüşme başlatıldığında native mikrofon izin diyaloğu görüntülenmektedir.

  • Mikrofon izni reddedildiğinde aynı kullanıcı aksiyonu içerisinde custom popup gösterilmemektedir.

  • Mikrofon izni reddedildikten sonraki denemelerde custom popup görüntülenmekte ve "Ayarlara Git" seçeneği sistem ayarları sayfasını açmaktadır.

  • Settings ekranında mikrofon izni verilip uygulamaya dönüldüğünde native taraf WebView'e granted state bildirmektedir.

  • App-level mikrofon izni verilmişse WebView yeniden oluşturulduğunda native permission popup tekrar gösterilmeden sesli görüşme başlatılabilmektedir.

  • Settings ekranında mikrofon izni kapatılıp uygulamaya dönüldüğünde native taraf WebView'e denied state bildirmektedir.

  • Dosya ekleme butonu native dosya seçici penceresini açmaktadır.

  • Sesli görüşme sırasında uygulamanın arka plana alınması durumunda görüşme sonlandırılmaktadır.

  • Cihaz dilinin değiştirilmesi durumunda arayüz dili buna uyum sağlamaktadır.